目 录CONTENT

文章目录

智能合约测试扫盲

懿曲折扇情
2026-03-14 / 0 评论 / 1 点赞 / 8 阅读 / 8,978 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2026-03-14,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
广告 广告

智能合约测试完全指南 - 从入门到精通

版本: v3.0
最后更新: 2026-03-14
目标: 让你看完后直接精通智能合约测试


📚 目录

第一部分:基础理论

  1. EVM原理深度解析
  2. 智能合约测试方法论
  3. 测试工具生态

第二部分:实战测试

  1. 功能测试实战
  2. 安全测试实战
  3. Gas优化测试
  4. 常见漏洞测试

第三部分:高级技术

  1. 高级测试技术
  2. DeFi协议测试
  3. NFT合约测试

第四部分:精通之路

  1. 测试最佳实践
  2. 精通路线图
  3. 真实案例分析

第一部分:基础理论


1. EVM原理深度解析

1.1 EVM是什么?

Ethereum Virtual Machine (EVM) 是以太坊的核心执行引擎,是一个:

  • ✅ 图灵完备的虚拟机
  • ✅ 基于栈的执行模型
  • ✅ 确定性的状态机
  • ✅ 分布式计算环境

1.2 EVM架构

┌─────────────────────────────────────────┐
│           Smart Contract Code           │
│        (Solidity/Vyper Source)          │
└────────────────┬────────────────────────┘
                 │ 编译
                 ▼
┌─────────────────────────────────────────┐
│            Bytecode (字节码)             │
│     (部署在区块链上的机器码)              │
└────────────────┬────────────────────────┘
                 │ 执行
                 ▼
┌─────────────────────────────────────────┐
│         EVM Execution Engine            │
│  ┌────────┐  ┌──────┐  ┌────────────┐  │
│  │ Stack  │  │Memory│  │  Storage   │  │
│  │(栈)    │  │(内存)│  │  (存储)    │  │
│  └────────┘  └──────┘  └────────────┘  │
│                                         │
│  ┌─────────────────────────────────┐   │
│  │        Gas Mechanism            │   │
│  │        (Gas消耗计算)            │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
                 │ 状态改变
                 ▼
┌─────────────────────────────────────────┐
│         World State (世界状态)          │
│   - Account Balance (账户余额)          │
│   - Contract Storage (合约存储)         │
│   - Nonce (交易计数)                    │
└─────────────────────────────────────────┘

1.3 EVM数据结构

1.3.1 Stack (栈)

特性:
- 最大深度: 1024
- 每个元素: 256位 (32字节)
- 操作: PUSH, POP, DUP, SWAP

示例操作:
PUSH1 0x05   // 将5压入栈
PUSH1 0x03   // 将3压入栈
ADD          // 弹出两个值,相加,结果压入栈

栈状态变化:
[]           // 初始
[5]          // PUSH1 0x05
[5, 3]       // PUSH1 0x03
[8]          // ADD (5+3=8)

1.3.2 Memory (内存)

特性:
- 线性字节数组
- 可动态扩展
- 按32字节访问
- Gas成本随使用增长

示例操作:
MSTORE(offset, value)  // 在offset位置存储32字节
MLOAD(offset)          // 从offset读取32字节

Memory布局:
0x00-0x3f: 保留区域 (用于哈希方法)
0x40-0x5f: 空闲内存指针
0x60-...: 用户数据

1.3.3 Storage (存储)

特性:
- 持久化存储 (写入区块链)
- Key-Value映射 (32字节 -> 32字节)
- 成本最高 (20,000 gas写入)
- 支持合约间持久化

示例操作:
SSTORE(key, value)  // 存储
SLOAD(key)          // 读取

Storage布局示例:
Slot 0: owner地址
Slot 1: totalSupply
Slot 2: balances映射的根
Slot 3: allowances映射的根

1.4 Gas机制详解

Gas计算公式

总Gas消耗 = Σ(操作码Gas成本) + 内存扩展成本 + 存储成本

常见操作Gas成本:
- ADD/SUB/MUL: 3 gas
- DIV/MOD: 5 gas
- SHA3: 30 gas + 6 gas/word
- SLOAD: 2100 gas (冷) / 100 gas (热)
- SSTORE: 20000 gas (新) / 5000 gas (修改) / 2900 gas (删除返还)
- CALL: 700 gas + 9000 gas (转账)
- CREATE: 32000 gas

Gas优化示例

// ❌ 不优化 - 多次SLOAD
function badExample() public view returns (uint) {
    uint total = 0;
    for (uint i = 0; i < count; i++) {  // 每次循环读取count
        total += items[i];
    }
    return total;
}
// Gas: ~2100 * count + ...

// ✅ 优化 - 缓存到内存
function goodExample() public view returns (uint) {
    uint total = 0;
    uint length = count;  // 只读取一次
    for (uint i = 0; i < length; i++) {
        total += items[i];
    }
    return total;
}
// Gas: 2100 + 3 * count + ...

1.5 EVM执行流程

1. 交易提交
   ↓
2. 语法验证 (签名、nonce、gas limit)
   ↓
3. 从发送者扣除 gas limit * gas price
   ↓
4. EVM开始执行字节码
   ↓
5. 逐条执行操作码
   - 消耗Gas
   - 修改状态
   - 触发事件
   ↓
6. 执行完成或revert
   ↓
7. 返还剩余Gas
   ↓
8. 状态提交到区块链

1.6 关键EVM概念

1.6.1 调用类型

// 1. CALL - 普通外部调用
targetContract.someFunction();
// - 切换执行上下文
// - msg.sender变为调用者
// - 有独立的gas限制

// 2. DELEGATECALL - 委托调用
address(targetContract).delegatecall(data);
// - 使用调用者的存储
// - msg.sender保持不变
// - 用于库和代理模式

// 3. STATICCALL - 静态调用
address(targetContract).staticcall(data);
// - 只读调用
// - 不能修改状态
// - 用于view/pure函数

1.6.2 事件和日志

event Transfer(address indexed from, address indexed to, uint256 value);

emit Transfer(sender, recipient, amount);

// 底层实现: LOG操作码
// LOG0, LOG1, LOG2, LOG3, LOG4
// 数字表示indexed参数数量

// 日志存储在交易收据中,不在状态树
// Gas成本: 375 + 375 * topics + 8 * data_size

2. 智能合约测试方法论

2.1 测试金字塔

        ┌──────────────┐
        │  E2E Tests   │  ← 少量,覆盖关键流程
        │  (端到端)     │
        └──────────────┘
       ┌────────────────┐
       │Integration Tests│ ← 中等数量,测试组件交互
       │   (集成测试)    │
       └────────────────┘
     ┌──────────────────────┐
     │    Unit Tests        │ ← 大量,测试单个函数
     │    (单元测试)         │
     └──────────────────────┘

2.2 测试分类

2.2.1 功能测试

目标: 验证合约按预期工作

describe("Token Transfer", function() {
  it("should transfer tokens correctly", async function() {
    // Arrange (准备)
    const [owner, user1, user2] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("MyToken");
    const token = await Token.deploy();
    await token.mint(user1.address, 1000);

    // Act (执行)
    await token.connect(user1).transfer(user2.address, 100);

    // Assert (断言)
    expect(await token.balanceOf(user2.address)).to.equal(100);
    expect(await token.balanceOf(user1.address)).to.equal(900);
  });
});

2.2.2 安全测试

目标: 发现漏洞和攻击向量

describe("Reentrancy Attack Prevention", function() {
  it("should prevent reentrancy attack", async function() {
    const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
    const attacker = await Attacker.deploy(vault.address);

    // 尝试重入攻击
    await expect(
      attacker.attack({value: ethers.parseEther("1")})
    ).to.be.revertedWith("ReentrancyGuard: reentrant call");
  });
});

2.2.3 边界条件测试

目标: 测试极端情况

describe("Edge Cases", function() {
  it("should handle zero transfer", async function() {
    await token.transfer(user.address, 0);
    // 验证没有状态改变
  });

  it("should handle max uint256", async function() {
    const maxUint = ethers.MaxUint256;
    await token.mint(user.address, maxUint);
    // 验证边界值
  });

  it("should revert on insufficient balance", async function() {
    await expect(
      token.transfer(user.address, 1000000)
    ).to.be.revertedWith("Insufficient balance");
  });
});

2.2.4 状态机测试

目标: 验证状态转换正确

describe("State Transitions", function() {
  it("should follow correct state flow", async function() {
    // Initial State
    expect(await contract.state()).to.equal(State.Pending);

    // Transition to Active
    await contract.activate();
    expect(await contract.state()).to.equal(State.Active);

    // Transition to Paused
    await contract.pause();
    expect(await contract.state()).to.equal(State.Paused);

    // Cannot go directly from Paused to Ended
    await expect(contract.end()).to.be.reverted;
  });
});

2.3 测试覆盖率目标

功能覆盖率目标:
├── 语句覆盖率 (Statement): 95%+
├── 分支覆盖率 (Branch): 90%+
├── 函数覆盖率 (Function): 100%
└── 行覆盖率 (Line): 95%+

安全测试覆盖:
├── OWASP Top 10 漏洞
├── SWC Registry 常见漏洞
├── 历史攻击案例
└── 特定协议风险

3. 测试工具生态

3.1 Hardhat (推荐)

优势:

  • ✅ 完整的开发环境
  • ✅ 强大的调试功能
  • ✅ 丰富的插件生态
  • ✅ 内置本地网络

安装和配置:

# 初始化项目
npm init -y
npm install --save-dev hardhat

# 创建Hardhat项目
npx hardhat

# 安装依赖
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
npm install --save-dev chai ethers

hardhat.config.js:

require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    hardhat: {
      chainId: 31337,
      forking: {
        url: process.env.MAINNET_RPC_URL,  // 用于Fork测试
        blockNumber: 19000000
      }
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  gasReporter: {
    enabled: true,
    currency: 'USD',
    coinmarketcap: process.env.COINMARKETCAP_API_KEY
  }
};

3.2 Foundry (高级用户)

优势:

  • ⚡ 极快的测试速度
  • ✅ 用Solidity写测试
  • ✅ 内置Fuzzing
  • ✅ 强大的调试工具

安装:

curl -L https://foundry.paradigm.xyz | bash
foundryup

测试示例:

// test/Token.t.sol
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token token;
    address user1;
    address user2;

    function setUp() public {
        token = new Token();
        user1 = address(0x1);
        user2 = address(0x2);

        token.mint(user1, 1000);
    }

    function testTransfer() public {
        vm.prank(user1);  // 模拟user1调用
        token.transfer(user2, 100);

        assertEq(token.balanceOf(user2), 100);
        assertEq(token.balanceOf(user1), 900);
    }

    function testFuzzTransfer(uint256 amount) public {
        vm.assume(amount <= 1000);  // 设置假设条件

        vm.prank(user1);
        token.transfer(user2, amount);

        assertEq(token.balanceOf(user2), amount);
        assertEq(token.balanceOf(user1), 1000 - amount);
    }
}

运行测试:

# 运行所有测试
forge test

# 显示详细输出
forge test -vvv

# 测试覆盖率
forge coverage

# Gas报告
forge test --gas-report

3.3 测试工具对比

特性 Hardhat Foundry Truffle
测试语言 JavaScript Solidity JavaScript
速度 中等 极快
调试 优秀 优秀 一般
Fuzzing 需插件 内置 不支持
学习曲线 平缓 陡峭 平缓
生态 丰富 增长中 成熟
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐

4. 功能测试实战

4.1 ERC20代币完整测试

4.1.1 合约代码

// contracts/MyToken.sol
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    uint256 public constant MAX_SUPPLY = 1000000 * 10**18;

    constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {}

    function mint(address to, uint256 amount) public onlyOwner {
        require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
        _mint(to, amount);
    }

    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

4.1.2 完整测试套件

// test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function() {
  let token;
  let owner;
  let user1;
  let user2;

  beforeEach(async function() {
    [owner, user1, user2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("MyToken");
    token = await Token.deploy();
    await token.waitForDeployment();
  });

  describe("Deployment", function() {
    it("Should set the right name and symbol", async function() {
      expect(await token.name()).to.equal("MyToken");
      expect(await token.symbol()).to.equal("MTK");
    });

    it("Should set the right owner", async function() {
      expect(await token.owner()).to.equal(owner.address);
    });

    it("Should have correct decimals", async function() {
      expect(await token.decimals()).to.equal(18);
    });

    it("Should start with zero total supply", async function() {
      expect(await token.totalSupply()).to.equal(0);
    });
  });

  describe("Minting", function() {
    it("Should mint tokens to address", async function() {
      const amount = ethers.parseEther("1000");
      await token.mint(user1.address, amount);

      expect(await token.balanceOf(user1.address)).to.equal(amount);
      expect(await token.totalSupply()).to.equal(amount);
    });

    it("Should emit Transfer event on mint", async function() {
      const amount = ethers.parseEther("1000");

      await expect(token.mint(user1.address, amount))
        .to.emit(token, "Transfer")
        .withArgs(ethers.ZeroAddress, user1.address, amount);
    });

    it("Should revert if non-owner tries to mint", async function() {
      const amount = ethers.parseEther("1000");

      await expect(
        token.connect(user1).mint(user2.address, amount)
      ).to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
    });

    it("Should revert if minting exceeds max supply", async function() {
      const maxSupply = await token.MAX_SUPPLY();
      const excessAmount = maxSupply + 1n;

      await expect(
        token.mint(user1.address, excessAmount)
      ).to.be.revertedWith("Exceeds max supply");
    });

    it("Should allow minting up to max supply", async function() {
      const maxSupply = await token.MAX_SUPPLY();
      await token.mint(user1.address, maxSupply);

      expect(await token.totalSupply()).to.equal(maxSupply);
    });
  });

  describe("Transfer", function() {
    beforeEach(async function() {
      await token.mint(user1.address, ethers.parseEther("1000"));
    });

    it("Should transfer tokens between accounts", async function() {
      const amount = ethers.parseEther("100");

      await token.connect(user1).transfer(user2.address, amount);

      expect(await token.balanceOf(user1.address)).to.equal(
        ethers.parseEther("900")
      );
      expect(await token.balanceOf(user2.address)).to.equal(amount);
    });

    it("Should emit Transfer event", async function() {
      const amount = ethers.parseEther("100");

      await expect(
        token.connect(user1).transfer(user2.address, amount)
      ).to.emit(token, "Transfer")
       .withArgs(user1.address, user2.address, amount);
    });

    it("Should revert on insufficient balance", async function() {
      const amount = ethers.parseEther("2000");

      await expect(
        token.connect(user1).transfer(user2.address, amount)
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
    });

    it("Should allow zero transfer", async function() {
      await token.connect(user1).transfer(user2.address, 0);
      // 验证没有错误
    });

    it("Should revert transfer to zero address", async function() {
      await expect(
        token.connect(user1).transfer(ethers.ZeroAddress, 100)
      ).to.be.revertedWithCustomError(token, "ERC20InvalidReceiver");
    });
  });

  describe("Approval and TransferFrom", function() {
    beforeEach(async function() {
      await token.mint(user1.address, ethers.parseEther("1000"));
    });

    it("Should approve tokens for delegated transfer", async function() {
      const amount = ethers.parseEther("100");

      await token.connect(user1).approve(user2.address, amount);

      expect(await token.allowance(user1.address, user2.address))
        .to.equal(amount);
    });

    it("Should emit Approval event", async function() {
      const amount = ethers.parseEther("100");

      await expect(
        token.connect(user1).approve(user2.address, amount)
      ).to.emit(token, "Approval")
       .withArgs(user1.address, user2.address, amount);
    });

    it("Should allow transferFrom with approval", async function() {
      const amount = ethers.parseEther("100");

      await token.connect(user1).approve(user2.address, amount);
      await token.connect(user2).transferFrom(
        user1.address,
        user2.address,
        amount
      );

      expect(await token.balanceOf(user2.address)).to.equal(amount);
      expect(await token.allowance(user1.address, user2.address))
        .to.equal(0);
    });

    it("Should revert transferFrom without approval", async function() {
      const amount = ethers.parseEther("100");

      await expect(
        token.connect(user2).transferFrom(
          user1.address,
          user2.address,
          amount
        )
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientAllowance");
    });

    it("Should revert transferFrom exceeding allowance", async function() {
      await token.connect(user1).approve(
        user2.address,
        ethers.parseEther("50")
      );

      await expect(
        token.connect(user2).transferFrom(
          user1.address,
          user2.address,
          ethers.parseEther("100")
        )
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientAllowance");
    });
  });

  describe("Burning", function() {
    beforeEach(async function() {
      await token.mint(user1.address, ethers.parseEther("1000"));
    });

    it("Should burn tokens", async function() {
      const burnAmount = ethers.parseEther("100");

      await token.connect(user1).burn(burnAmount);

      expect(await token.balanceOf(user1.address)).to.equal(
        ethers.parseEther("900")
      );
      expect(await token.totalSupply()).to.equal(
        ethers.parseEther("900")
      );
    });

    it("Should emit Transfer event to zero address", async function() {
      const burnAmount = ethers.parseEther("100");

      await expect(token.connect(user1).burn(burnAmount))
        .to.emit(token, "Transfer")
        .withArgs(user1.address, ethers.ZeroAddress, burnAmount);
    });

    it("Should revert burn with insufficient balance", async function() {
      await expect(
        token.connect(user1).burn(ethers.parseEther("2000"))
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
    });
  });

  describe("Edge Cases", function() {
    it("Should handle max uint256 approval", async function() {
      await token.connect(user1).approve(user2.address, ethers.MaxUint256);

      expect(await token.allowance(user1.address, user2.address))
        .to.equal(ethers.MaxUint256);
    });

    it("Should not decrease infinite allowance", async function() {
      await token.mint(user1.address, ethers.parseEther("1000"));
      await token.connect(user1).approve(user2.address, ethers.MaxUint256);

      await token.connect(user2).transferFrom(
        user1.address,
        user2.address,
        ethers.parseEther("100")
      );

      // 无限授权不应减少
      expect(await token.allowance(user1.address, user2.address))
        .to.equal(ethers.MaxUint256);
    });
  });

  describe("Gas Optimization Tests", function() {
    it("Should compare gas costs for different operations", async function() {
      const amount = ethers.parseEther("100");

      // Mint gas cost
      const mintTx = await token.mint(user1.address, amount);
      const mintReceipt = await mintTx.wait();
      console.log("Mint gas:", mintReceipt.gasUsed.toString());

      // Transfer gas cost
      const transferTx = await token.connect(user1).transfer(
        user2.address,
        amount
      );
      const transferReceipt = await transferTx.wait();
      console.log("Transfer gas:", transferReceipt.gasUsed.toString());
    });
  });
});

4.2 运行测试

# 运行所有测试
npx hardhat test

# 运行特定测试文件
npx hardhat test test/MyToken.test.js

# 显示gas报告
REPORT_GAS=true npx hardhat test

# 测试覆盖率
npx hardhat coverage

5. 安全测试实战

5.1 重入攻击测试

5.1.1 漏洞合约

// contracts/VulnerableBank.sol
pragma solidity ^0.8.20;

contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // ❌ 漏洞: 先转账后更新状态
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // 危险: 外部调用在状态更新之前
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        balances[msg.sender] -= amount;  // 状态更新太晚
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

5.1.2 攻击合约

// contracts/ReentrancyAttacker.sol
pragma solidity ^0.8.20;

interface IVulnerableBank {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
}

contract ReentrancyAttacker {
    IVulnerableBank public bank;
    uint256 public attackAmount;
    uint256 public attackCount;

    constructor(address _bankAddress) {
        bank = IVulnerableBank(_bankAddress);
    }

    function attack() public payable {
        attackAmount = msg.value;
        attackCount = 0;

        // 存入
        bank.deposit{value: attackAmount}();

        // 发起攻击
        bank.withdraw(attackAmount);
    }

    // 接收ETH时重入
    receive() external payable {
        attackCount++;

        // 重入攻击
        if (address(bank).balance >= attackAmount && attackCount < 5) {
            bank.withdraw(attackAmount);
        }
    }

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

5.1.3 重入攻击测试

// test/ReentrancyAttack.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Reentrancy Attack Test", function() {
  let bank;
  let attacker;
  let owner;
  let user;

  beforeEach(async function() {
    [owner, user] = await ethers.getSigners();

    // 部署漏洞合约
    const VulnerableBank = await ethers.getContractFactory("VulnerableBank");
    bank = await VulnerableBank.deploy();

    // 正常用户存入5 ETH
    await bank.connect(user).deposit({value: ethers.parseEther("5")});

    // 部署攻击合约
    const ReentrancyAttacker = await ethers.getContractFactory(
      "ReentrancyAttacker"
    );
    attacker = await ReentrancyAttacker.deploy(await bank.getAddress());
  });

  it("Should demonstrate reentrancy attack", async function() {
    const bankInitialBalance = await ethers.provider.getBalance(
      await bank.getAddress()
    );
    console.log("Bank initial balance:", ethers.formatEther(bankInitialBalance));

    // 攻击者只存入1 ETH
    const attackAmount = ethers.parseEther("1");

    // 执行攻击
    await attacker.attack({value: attackAmount});

    // 检查结果
    const bankFinalBalance = await ethers.provider.getBalance(
      await bank.getAddress()
    );
    const attackerBalance = await attacker.getBalance();

    console.log("Bank final balance:", ethers.formatEther(bankFinalBalance));
    console.log("Attacker balance:", ethers.formatEther(attackerBalance));
    console.log("Stolen amount:", ethers.formatEther(
      attackAmount - bankFinalBalance
    ));

    // 验证攻击成功
    expect(attackerBalance).to.be.gt(attackAmount);
    expect(bankFinalBalance).to.be.lt(bankInitialBalance);
  });
});

5.1.4 安全修复版本

// contracts/SecureBank.sol
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // ✅ 修复1: 使用 ReentrancyGuard
    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // ✅ 修复2: 先更新状态
        balances[msg.sender] -= amount;

        // ✅ 修复3: 最后外部调用
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }

    // ✅ 修复4: 使用 Pull Payment 模式
    mapping(address => uint256) public pendingWithdrawals;

    function requestWithdrawal(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        pendingWithdrawals[msg.sender] += amount;
    }

    function executeWithdrawal() public nonReentrant {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "No pending withdrawal");

        pendingWithdrawals[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

5.1.5 测试修复效果

describe("Secure Bank Test", function() {
  it("Should prevent reentrancy attack", async function() {
    const SecureBank = await ethers.getContractFactory("SecureBank");
    const secureBank = await SecureBank.deploy();

    await secureBank.connect(user).deposit({value: ethers.parseEther("5")});

    const SecureAttacker = await ethers.getContractFactory(
      "ReentrancyAttacker"
    );
    const secureAttacker = await SecureAttacker.deploy(
      await secureBank.getAddress()
    );

    // 攻击应该失败
    await expect(
      secureAttacker.attack({value: ethers.parseEther("1")})
    ).to.be.reverted;
  });
});

5.2 整数溢出测试

5.2.1 漏洞示例 (Solidity < 0.8.0)

// contracts/VulnerableToken.sol
pragma solidity ^0.7.6;  // 旧版本,没有自动溢出检查

contract VulnerableToken {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // ❌ 漏洞: 可能整数下溢
    function withdraw(uint256 amount) public {
        balances[msg.sender] -= amount;  // 没有检查余额
        payable(msg.sender).transfer(amount);
    }
}

5.2.2 溢出攻击测试

describe("Integer Overflow Attack", function() {
  it("Should demonstrate underflow attack (Solidity < 0.8)", async function() {
    // 注意: 需要使用 Solidity 0.7.x 编译

    const amount = ethers.parseEther("1");

    // 攻击者余额为0
    expect(await vulnerableToken.balances(attacker.address)).to.equal(0);

    // 尝试提取1 ETH (余额不足)
    // 在 0.7.x 中会下溢变成超大数
    await vulnerableToken.connect(attacker).withdraw(amount);

    // 余额下溢
    const balance = await vulnerableToken.balances(attacker.address);
    expect(balance).to.be.gt(ethers.MaxUint256 / 2n);
  });
});

5.2.3 安全版本 (Solidity 0.8+)

// contracts/SecureToken.sol
pragma solidity ^0.8.20;  // ✅ 自动溢出检查

contract SecureToken {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;  // ✅ 自动检查溢出
    }

    function withdraw(uint256 amount) public {
        // ✅ 会自动revert如果余额不足
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }
}

5.3 访问控制测试

5.3.1 测试所有权限

describe("Access Control", function() {
  let contract;
  let owner;
  let admin;
  let user;

  beforeEach(async function() {
    [owner, admin, user] = await ethers.getSigners();

    const Contract = await ethers.getContractFactory("MyContract");
    contract = await Contract.deploy();

    // 设置admin角色
    await contract.grantRole(await contract.ADMIN_ROLE(), admin.address);
  });

  describe("Owner Functions", function() {
    it("Should allow owner to call owner-only functions", async function() {
      await expect(contract.connect(owner).ownerFunction())
        .to.not.be.reverted;
    });

    it("Should reject non-owner calls", async function() {
      await expect(contract.connect(user).ownerFunction())
        .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount");
    });

    it("Should transfer ownership", async function() {
      await contract.transferOwnership(admin.address);
      expect(await contract.owner()).to.equal(admin.address);
    });
  });

  describe("Role-Based Access", function() {
    it("Should allow admin to call admin functions", async function() {
      await expect(contract.connect(admin).adminFunction())
        .to.not.be.reverted;
    });

    it("Should reject non-admin calls", async function() {
      await expect(contract.connect(user).adminFunction())
        .to.be.revertedWith("Missing ADMIN_ROLE");
    });

    it("Should grant and revoke roles", async function() {
      // 授予角色
      await contract.grantRole(await contract.ADMIN_ROLE(), user.address);
      expect(await contract.hasRole(await contract.ADMIN_ROLE(), user.address))
        .to.be.true;

      // 撤销角色
      await contract.revokeRole(await contract.ADMIN_ROLE(), user.address);
      expect(await contract.hasRole(await contract.ADMIN_ROLE(), user.address))
        .to.be.false;
    });
  });
});

5.4 前端运行攻击 (Front-Running)

5.4.1 漏洞场景

// contracts/VulnerableAuction.sol
pragma solidity ^0.8.20;

contract VulnerableAuction {
    address public highestBidder;
    uint256 public highestBid;

    // ❌ 漏洞: 可被前端运行
    function bid() public payable {
        require(msg.value > highestBid, "Bid too low");

        // 退还前一个最高出价
        if (highestBidder != address(0)) {
            payable(highestBidder).transfer(highestBid);
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }
}

5.4.2 前端运行测试

describe("Front-Running Attack", function() {
  it("Should demonstrate front-running vulnerability", async function() {
    const [user1, user2] = await ethers.getSigners();

    // User1 出价 1 ETH
    await auction.connect(user1).bid({value: ethers.parseEther("1")});

    // User2 看到 User1 的交易,立即出更高价
    // 并设置更高的 gasPrice 让交易先执行
    await auction.connect(user2).bid({
      value: ethers.parseEther("1.01"),
      gasPrice: ethers.parseUnits("100", "gwei")  // 更高gas
    });

    // User2 通过前端运行成为最高出价者
    expect(await auction.highestBidder()).to.equal(user2.address);
  });
});

5.4.3 防护方案:承诺-揭示模式

// contracts/SecureAuction.sol
pragma solidity ^0.8.20;

contract SecureAuction {
    mapping(address => bytes32) public commitments;
    mapping(address => uint256) public bids;

    uint256 public commitPhaseEnd;
    uint256 public revealPhaseEnd;

    constructor(uint256 commitDuration, uint256 revealDuration) {
        commitPhaseEnd = block.timestamp + commitDuration;
        revealPhaseEnd = commitPhaseEnd + revealDuration;
    }

    // 阶段1: 提交承诺 (隐藏出价)
    function commit(bytes32 commitment) public {
        require(block.timestamp < commitPhaseEnd, "Commit phase ended");
        commitments[msg.sender] = commitment;
    }

    // 阶段2: 揭示出价
    function reveal(uint256 amount, bytes32 nonce) public payable {
        require(block.timestamp >= commitPhaseEnd, "Commit phase not ended");
        require(block.timestamp < revealPhaseEnd, "Reveal phase ended");
        require(msg.value == amount, "Amount mismatch");

        // 验证承诺
        bytes32 commitment = keccak256(abi.encodePacked(amount, nonce));
        require(commitments[msg.sender] == commitment, "Invalid commitment");

        bids[msg.sender] = amount;
    }
}

6. Gas优化测试

6.1 Gas测试基准

describe("Gas Optimization", function() {
  it("Should compare gas costs of different implementations", async function() {
    // 方法1: 多次 SLOAD
    const tx1 = await contract.methodWithMultipleSLOAD();
    const receipt1 = await tx1.wait();
    console.log("Multiple SLOAD gas:", receipt1.gasUsed.toString());

    // 方法2: 缓存到内存
    const tx2 = await contract.methodWithCaching();
    const receipt2 = await tx2.wait();
    console.log("With caching gas:", receipt2.gasUsed.toString());

    // 计算节省
    const saved = receipt1.gasUsed - receipt2.gasUsed;
    console.log("Gas saved:", saved.toString());

    expect(receipt2.gasUsed).to.be.lt(receipt1.gasUsed);
  });
});

6.2 常见Gas优化技巧

6.2.1 缓存存储变量

// ❌ 不优化
function sumArray() public view returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < array.length; i++) {  // 每次循环读取 length
        total += array[i];
    }
    return total;
}
// Gas: ~2100 * iterations

// ✅ 优化
function sumArrayOptimized() public view returns (uint256) {
    uint256 total = 0;
    uint256 length = array.length;  // 缓存到内存
    for (uint256 i = 0; i < length; i++) {
        total += array[i];
    }
    return total;
}
// Gas: ~2100 + 3 * iterations

6.2.2 使用 uint256 而不是小类型

// ❌ Gas浪费
uint8 a = 1;
uint8 b = 2;
uint8 c = a + b;  // EVM需要额外转换

// ✅ Gas优化
uint256 a = 1;
uint256 b = 2;
uint256 c = a + b;  // EVM原生支持

6.2.3 短路求值

// ✅ 将便宜的检查放前面
require(amount > 0 && balances[msg.sender] >= amount, "Invalid");

// ❌ 昂贵的操作在前
require(balances[msg.sender] >= amount && amount > 0, "Invalid");

6.2.4 批量操作

// ❌ 单独操作
function transferMultiple(address[] memory recipients, uint256[] memory amounts)
    public
{
    for (uint256 i = 0; i < recipients.length; i++) {
        transfer(recipients[i], amounts[i]);  // 每次都emit event
    }
}

// ✅ 批量操作
function batchTransfer(address[] memory recipients, uint256[] memory amounts)
    public
{
    uint256 totalAmount = 0;
    for (uint256 i = 0; i < recipients.length; i++) {
        totalAmount += amounts[i];
        _balances[recipients[i]] += amounts[i];
    }
    _balances[msg.sender] -= totalAmount;
    emit BatchTransfer(recipients, amounts);  // 只emit一次
}

7. 常见漏洞测试

7.1 漏洞检查清单

## 🔴 高危漏洞

- [ ] 重入攻击 (Reentrancy)
- [ ] 整数溢出/下溢 (Integer Overflow/Underflow)
- [ ] 访问控制缺陷 (Access Control)
- [ ] 未检查的外部调用 (Unchecked External Calls)
- [ ] 委托调用漏洞 (Delegatecall Vulnerability)
- [ ] 签名重放 (Signature Replay)
- [ ] 前端运行 (Front-Running)

## ⚠️ 中危漏洞

- [ ] DoS攻击 (Denial of Service)
- [ ] 时间戳依赖 (Timestamp Dependence)
- [ ] 随机数可预测 (Weak Randomness)
- [ ] 交易顺序依赖 (Transaction Ordering Dependence)
- [ ] Gas Limit DoS
- [ ] 未初始化的存储指针

## ℹ️ 低危问题

- [ ] 未检查的返回值
- [ ] 浮点和精度问题
- [ ] 废弃函数使用
- [ ] Gas优化问题
- [ ] 事件缺失

7.2 时间戳依赖测试

// contracts/VulnerableTimelock.sol
contract VulnerableTimelock {
    uint256 public unlockTime;

    constructor(uint256 duration) {
        // ❌ 依赖 block.timestamp (可被矿工操纵)
        unlockTime = block.timestamp + duration;
    }

    function withdraw() public {
        require(block.timestamp >= unlockTime, "Still locked");
        // 提取资金
    }
}
// test/TimestampDependence.test.js
describe("Timestamp Dependence", function() {
  it("Should demonstrate timestamp manipulation", async function() {
    const duration = 3600;  // 1小时
    const timelock = await Timelock.deploy(duration);

    // 获取当前时间
    const block = await ethers.provider.getBlock("latest");
    console.log("Current timestamp:", block.timestamp);

    // 时间旅行 (Hardhat功能)
    await ethers.provider.send("evm_increaseTime", [duration + 1]);
    await ethers.provider.send("evm_mine");

    // 现在可以提取
    await expect(timelock.withdraw()).to.not.be.reverted;
  });

  it("Should test timestamp manipulation vulnerability", async function() {
    // 矿工可以在一定范围内操纵时间戳 (通常±15秒)
    await ethers.provider.send("evm_setNextBlockTimestamp", [
      block.timestamp + 15
    ]);
    await ethers.provider.send("evm_mine");

    // 测试在时间戳被操纵的情况下的行为
  });
});

7.3 DoS攻击测试

// contracts/VulnerableAuction.sol
contract VulnerableAuction {
    address public highestBidder;
    uint256 public highestBid;

    function bid() public payable {
        require(msg.value > highestBid);

        // ❌ 漏洞: 如果前一个出价者的合约revert,整个拍卖就会DoS
        if (highestBidder != address(0)) {
            payable(highestBidder).transfer(highestBid);  // 可能失败
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }
}

// contracts/MaliciousBidder.sol
contract MaliciousBidder {
    VulnerableAuction auction;

    constructor(address _auction) {
        auction = VulnerableAuction(_auction);
    }

    function attack() public payable {
        auction.bid{value: msg.value}();
    }

    // ❌ 拒绝接收ETH,导致拍卖DoS
    receive() external payable {
        revert("I refuse to accept refund");
    }
}
// test/DoS.test.js
describe("DoS Attack", function() {
  it("Should demonstrate DoS vulnerability", async function() {
    // 恶意出价者出价
    await maliciousBidder.attack({value: ethers.parseEther("1")});

    // 正常用户尝试出更高价,但会失败
    await expect(
      auction.connect(normalUser).bid({value: ethers.parseEther("2")})
    ).to.be.reverted;  // 因为无法退款给恶意合约

    console.log("拍卖被DoS攻击锁定");
  });
});

7.3.1 安全修复:Pull Payment模式

// contracts/SecureAuction.sol
contract SecureAuction {
    address public highestBidder;
    uint256 public highestBid;

    mapping(address => uint256) public pendingReturns;  // ✅ Pull模式

    function bid() public payable {
        require(msg.value > highestBid);

        if (highestBidder != address(0)) {
            // ✅ 不立即转账,而是记录待领取金额
            pendingReturns[highestBidder] += highestBid;
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    // ✅ 用户主动领取退款
    function withdrawRefund() public {
        uint256 amount = pendingReturns[msg.sender];
        require(amount > 0);

        pendingReturns[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        if (!success) {
            pendingReturns[msg.sender] = amount;  // 失败则恢复
        }
    }
}

8. 高级测试技术

8.1 Fuzzing测试 (模糊测试)

8.1.1 使用Foundry Fuzzing

// test/FuzzTest.t.sol
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract FuzzTest is Test {
    Token token;

    function setUp() public {
        token = new Token();
    }

    // Foundry会自动生成随机输入
    function testFuzzTransfer(address to, uint256 amount) public {
        // 设置假设条件
        vm.assume(to != address(0));
        vm.assume(amount <= type(uint256).max / 2);

        token.mint(address(this), amount);
        token.transfer(to, amount);

        assertEq(token.balanceOf(to), amount);
    }

    // 测试边界条件
    function testFuzzNoOverflow(uint256 a, uint256 b) public {
        vm.assume(a <= type(uint256).max - b);  // 防止溢出

        uint256 result = token.add(a, b);
        assertEq(result, a + b);
    }
}
# 运行fuzzing测试
forge test --match-test testFuzz

# 增加测试次数
forge test --match-test testFuzz --fuzz-runs 10000

8.1.2 Echidna Fuzzing

安装Echidna:

docker pull trailofbits/eth-security-toolbox

Echidna配置:

# echidna.yaml
testMode: assertion
testLimit: 50000
timeout: 300
coverage: true
corpusDir: corpus

测试合约:

// contracts/EchidnaTest.sol
pragma solidity ^0.8.20;

import "./Token.sol";

contract EchidnaTest {
    Token token;

    constructor() {
        token = new Token();
    }

    // Echidna会尝试让这个断言失败
    function echidna_balance_never_negative() public view returns (bool) {
        return token.balanceOf(address(this)) >= 0;
    }

    function echidna_total_supply_conservation() public view returns (bool) {
        // 总供应量应该等于所有余额之和
        return token.totalSupply() == token.balanceOf(address(this));
    }
}
# 运行Echidna
echidna-test contracts/EchidnaTest.sol --config echidna.yaml

8.2 形式化验证

8.2.1 SMT Solver验证

// contracts/VerifiedContract.sol
pragma solidity ^0.8.20;

contract VerifiedContract {
    uint256 public value;

    // SMTChecker规范
    /// @custom:smtchecker assert-verified
    function increment() public {
        uint256 oldValue = value;
        value++;

        // 形式化验证条件
        assert(value == oldValue + 1);
        assert(value > oldValue);
    }
}

编译时启用SMTChecker:

// hardhat.config.js
module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: { enabled: true },
      modelChecker: {
        engine: "chc",  // 使用SMT solver
        targets: ["assert", "underflow", "overflow"],
      }
    }
  }
};

8.3 静态分析

8.3.1 Slither

# 安装
pip3 install slither-analyzer

# 运行分析
slither contracts/MyToken.sol

# 生成报告
slither contracts/MyToken.sol --json report.json

# 检查特定问题
slither contracts/MyToken.sol --detect reentrancy-eth

8.3.2 Mythril

# 安装
pip3 install mythril

# 分析合约
myth analyze contracts/MyToken.sol

# 深度分析
myth analyze contracts/MyToken.sol --execution-timeout 900

8.4 符号执行

# 使用Manticore进行符号执行
pip3 install manticore

# 分析合约
manticore contracts/MyToken.sol --contract MyToken

9. DeFi协议测试

9.1 AMM (自动做市商) 测试

// contracts/SimpleAMM.sol
pragma solidity ^0.8.20;

contract SimpleAMM {
    uint256 public reserveA;
    uint256 public reserveB;

    function addLiquidity(uint256 amountA, uint256 amountB) public {
        reserveA += amountA;
        reserveB += amountB;
    }

    // x * y = k
    function swap(uint256 amountIn, bool swapAForB) public returns (uint256) {
        uint256 amountOut;

        if (swapAForB) {
            amountOut = (reserveB * amountIn) / (reserveA + amountIn);
            reserveA += amountIn;
            reserveB -= amountOut;
        } else {
            amountOut = (reserveA * amountIn) / (reserveB + amountIn);
            reserveB += amountIn;
            reserveA -= amountOut;
        }

        return amountOut;
    }

    function getSpotPrice() public view returns (uint256) {
        return (reserveB * 1e18) / reserveA;
    }
}
// test/AMM.test.js
describe("AMM Tests", function() {
  let amm;

  beforeEach(async function() {
    const AMM = await ethers.getContractFactory("SimpleAMM");
    amm = await AMM.deploy();

    // 初始流动性: 1000 tokenA, 1000 tokenB
    await amm.addLiquidity(1000, 1000);
  });

  it("Should calculate correct swap amount", async function() {
    // 用100 tokenA换取tokenB
    const amountOut = await amm.swap.staticCall(100, true);

    // x * y = k
    // (1000 + 100) * (1000 - amountOut) = 1000 * 1000
    // amountOut ≈ 90.9
    expect(amountOut).to.be.closeTo(90, 1);
  });

  it("Should maintain constant product", async function() {
    const k = (await amm.reserveA()) * (await amm.reserveB());

    await amm.swap(100, true);

    const newK = (await amm.reserveA()) * (await amm.reserveB());

    expect(newK).to.be.gte(k);  // k只增不减 (因为手续费)
  });

  it("Should have correct price impact", async function() {
    const priceBefore = await amm.getSpotPrice();

    // 大额交易会造成价格滑点
    await amm.swap(500, true);

    const priceAfter = await amm.getSpotPrice();

    // tokenB价格应该上升 (相对于tokenA)
    expect(priceAfter).to.be.gt(priceBefore);
  });

  describe("Flash Loan Attack Protection", function() {
    it("Should prevent price manipulation via flash loan", async function() {
      // 模拟闪电贷攻击
      const largeAmount = 10000;

      // 攻击者借大量tokenA
      const priceBefore = await amm.getSpotPrice();

      // 大量买入tokenB,操纵价格
      await amm.swap(largeAmount, true);
      const priceManipulated = await amm.getSpotPrice();

      // 在其他协议利用被操纵的价格

      // 卖出tokenB,还原价格
      const tokenBAmount = await amm.reserveB();
      await amm.swap(tokenBAmount / 2, false);

      const priceAfter = await amm.getSpotPrice();

      // 验证是否有防护机制
      // (实际应该使用TWAP或其他预言机)
    });
  });
});

9.2 借贷协议测试

describe("Lending Protocol", function() {
  describe("Collateral Management", function() {
    it("Should calculate correct health factor", async function() {
      // 健康因子 = (抵押物价值 * 清算阈值) / 借款价值
      const collateralValue = ethers.parseEther("100");
      const borrowedValue = ethers.parseEther("50");
      const liquidationThreshold = 80;  // 80%

      await lending.deposit(collateralValue);
      await lending.borrow(borrowedValue);

      const healthFactor = await lending.getHealthFactor(user.address);

      // (100 * 0.8) / 50 = 1.6
      expect(healthFactor).to.equal(ethers.parseEther("1.6"));
    });

    it("Should liquidate under-collateralized position", async function() {
      await lending.deposit(ethers.parseEther("100"));
      await lending.borrow(ethers.parseEther("80"));

      // 模拟价格下跌
      await oracle.setPrice(ethers.parseEther("0.5"));

      // 健康因子 < 1,可以清算
      const healthFactor = await lending.getHealthFactor(user.address);
      expect(healthFactor).to.be.lt(ethers.parseEther("1"));

      // 清算
      await expect(
        lending.connect(liquidator).liquidate(user.address)
      ).to.not.be.reverted;
    });
  });
});

10. NFT合约测试

10.1 ERC721测试

describe("NFT Contract", function() {
  describe("Minting", function() {
    it("Should mint NFT correctly", async function() {
      await nft.mint(user.address, 1);

      expect(await nft.ownerOf(1)).to.equal(user.address);
      expect(await nft.balanceOf(user.address)).to.equal(1);
    });

    it("Should increment token ID", async function() {
      await nft.mint(user1.address, 1);
      await nft.mint(user2.address, 2);

      expect(await nft.totalSupply()).to.equal(2);
    });

    it("Should revert double mint", async function() {
      await nft.mint(user.address, 1);

      await expect(
        nft.mint(user.address, 1)
      ).to.be.revertedWith("Token already minted");
    });
  });

  describe("Metadata", function() {
    it("Should return correct token URI", async function() {
      await nft.mint(user.address, 1);

      const uri = await nft.tokenURI(1);
      expect(uri).to.equal("https://api.example.com/metadata/1");
    });

    it("Should support ERC721Metadata interface", async function() {
      // ERC165 interface check
      const interfaceId = "0x5b5e139f";  // ERC721Metadata
      expect(await nft.supportsInterface(interfaceId)).to.be.true;
    });
  });

  describe("Royalties (ERC2981)", function() {
    it("Should return correct royalty info", async function() {
      await nft.mint(creator.address, 1);

      const salePrice = ethers.parseEther("1");
      const [receiver, royaltyAmount] = await nft.royaltyInfo(1, salePrice);

      expect(receiver).to.equal(creator.address);
      expect(royaltyAmount).to.equal(salePrice * 5n / 100n);  // 5%
    });
  });
});

11. 测试最佳实践

11.1 测试组织结构

test/
├── unit/                    # 单元测试
│   ├── Token.test.js
│   ├── Vault.test.js
│   └── Governance.test.js
├── integration/             # 集成测试
│   ├── TokenVault.test.js
│   └── FullProtocol.test.js
├── security/                # 安全测试
│   ├── Reentrancy.test.js
│   ├── AccessControl.test.js
│   └── FrontRunning.test.js
├── fuzzing/                 # 模糊测试
│   └── TokenFuzz.t.sol
└── gas/                     # Gas测试
    └── GasOptimization.test.js

11.2 测试命名规范

describe("合约名", function() {
  describe("功能模块", function() {
    it("should [期望行为] when [条件]", async function() {
      // 测试代码
    });

    it("should revert when [错误条件]", async function() {
      // 测试错误情况
    });
  });
});

// 示例:
describe("Token", function() {
  describe("Transfer", function() {
    it("should transfer tokens when sender has sufficient balance", async function() {
      // ...
    });

    it("should revert when sender has insufficient balance", async function() {
      // ...
    });
  });
});

11.3 持续集成 (CI)

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npx hardhat test

      - name: Run coverage
        run: npx hardhat coverage

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage.json

      - name: Run Slither
        uses: crytic/slither-action@v0.3.0
        with:
          target: contracts/

12. 精通路线图

12.1 初级阶段 (1-2个月)

学习目标:

  • ✅ 理解EVM基础原理
  • ✅ 掌握Solidity语法
  • ✅ 会写基础单元测试
  • ✅ 理解常见漏洞

实战项目:

  1. ERC20代币合约 + 完整测试
  2. 简单的多签钱包
  3. NFT铸造合约

学习资源:

  • Solidity官方文档
  • CryptoZombies教程
  • OpenZeppelin合约库

12.2 中级阶段 (3-6个月)

学习目标:

  • ✅ 精通测试框架 (Hardhat/Foundry)
  • ✅ 掌握集成测试
  • ✅ 会使用Fuzzing
  • ✅ 理解DeFi协议原理

实战项目:

  1. AMM DEX实现 + 测试
  2. 借贷协议
  3. Staking合约
  4. 复现历史攻击案例

学习资源:

  • Damn Vulnerable DeFi挑战
  • Ethernaut CTF
  • Trail of Bits安全指南

12.3 高级阶段 (6-12个月)

学习目标:

  • ✅ 掌握形式化验证
  • ✅ 能进行安全审计
  • ✅ 精通Gas优化
  • ✅ 理解MEV和前端运行

实战项目:

  1. 完整的DeFi协议
  2. DAO治理系统
  3. 跨链桥
  4. 参与真实项目审计

学习资源:

  • Secureum Bootcamp
  • 阅读审计报告 (Trail of Bits, OpenZeppelin, etc.)
  • Code4rena竞赛

12.4 专家阶段 (持续学习)

目标:

  • 🎯 成为安全审计专家
  • 🎯 发现0day漏洞
  • 🎯 设计安全协议
  • 🎯 贡献开源工具

13. 真实案例分析

13.1 The DAO攻击 (2016)

漏洞: 重入攻击

// 简化的漏洞代码
function withdraw(uint amount) {
    if (balances[msg.sender] >= amount) {
        // ❌ 先转账
        msg.sender.call.value(amount)();
        // ❌ 后更新状态
        balances[msg.sender] -= amount;
    }
}

损失: 360万ETH (~$60M)

教训:

  • 总是遵循 Checks-Effects-Interactions 模式
  • 使用 ReentrancyGuard
  • 在状态更新前不要进行外部调用

13.2 Poly Network攻击 (2021)

漏洞: 权限控制缺陷

损失: $611M

教训:

  • 严格的权限管理
  • 多重签名
  • 时间锁

13.3 测试这些漏洞

describe("Historical Attack Reproductions", function() {
  describe("The DAO Attack", function() {
    it("Should demonstrate reentrancy attack", async function() {
      // 复现The DAO攻击逻辑
      // ...
    });

    it("Should show how fix prevents attack", async function() {
      // 测试修复后的版本
      // ...
    });
  });
});

总结:精通智能合约测试的关键

✅ 核心知识

  1. 深入理解EVM

    • 执行模型
    • Gas机制
    • 存储结构
  2. 掌握测试方法论

    • 单元测试
    • 集成测试
    • 安全测试
    • Fuzzing
  3. 熟悉工具生态

    • Hardhat/Foundry
    • Slither/Mythril
    • Echidna
  4. 了解常见漏洞

    • 重入攻击
    • 整数溢出
    • 访问控制
    • 前端运行

✅ 实践路径

  1. 读代码: 阅读100+优秀合约
  2. 写测试: 为50+合约写完整测试
  3. 找漏洞: 完成所有CTF挑战
  4. 做审计: 参与真实项目审计

✅ 持续学习

  • 📚 每周阅读审计报告
  • 🔍 关注最新攻击事件
  • 💻 参与开源项目
  • 🏆 参加安全竞赛

附录:速查表

A. 常用测试命令

# Hardhat
npx hardhat test
npx hardhat test --grep "specific test"
npx hardhat coverage
REPORT_GAS=true npx hardhat test

# Foundry
forge test
forge test -vvv
forge test --match-test testFuzz
forge coverage
forge snapshot

# 静态分析
slither contracts/
myth analyze contracts/MyContract.sol

B. 断言速查

// Chai断言
expect(value).to.equal(expected);
expect(value).to.be.gt(other);
expect(value).to.be.lt(other);
expect(await promise).to.be.reverted;
expect(await promise).to.be.revertedWith("Error message");
expect(await promise).to.emit(contract, "Event");

// Foundry断言
assertEq(a, b);
assertGt(a, b);
assertLt(a, b);
vm.expectRevert();
vm.expectEmit(true, true, false, true);

C. 有用的资源


最后更新: 2026-03-14
作者: Smart Contract Security Team
版权: MIT License


看完这份文档,配合大量实践,你就能精通智能合约测试!

1

评论区