HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • Web3 完整技术体系

    • Web3 完整技术体系
    • 区块链基础与密码学
    • 第一章:比特币原理与实现
    • 第二章:以太坊架构与核心概念
    • Solidity智能合约开发基础
    • 04-Solidity进阶与安全
    • 05-ERC标准详解
    • 06-DeFi核心协议-去中心化交易所
    • 07-DeFi核心协议-借贷协议
    • 08-DeFi核心协议-稳定币
    • NFT与元宇宙
    • Layer2扩容方案
    • 跨链技术
    • Web3前端开发
    • 链下数据与预言机
    • 智能合约测试与部署
    • MEV与交易优化
    • DAO治理
    • 项目实战:完整DeFi借贷协议

智能合约测试与部署

1. 测试框架概述

智能合约测试是确保代码安全性和正确性的关键环节。与传统软件开发不同,智能合约部署后无法修改,因此测试的重要性不言而喻。

1.1 测试的重要性

智能合约测试的核心目标:

  • 安全性验证:防止资金损失和漏洞利用
  • 功能正确性:确保业务逻辑符合预期
  • Gas优化验证:降低用户交易成本
  • 边界条件检查:处理极端情况
  • 回归测试:确保新功能不破坏现有功能

1.2 测试类型

测试金字塔
    /\
   /集\
  /成测\
 /试  试\
/__单元测试__\
  • 单元测试:测试单个函数或合约
  • 集成测试:测试多个合约交互
  • Fork测试:在主网Fork上测试
  • 模糊测试:随机输入测试边界条件
  • 不变性测试:验证系统不变量

2. Hardhat 测试框架

2.1 Hardhat 项目初始化

# 创建项目目录
mkdir defi-protocol-test
cd defi-protocol-test

# 初始化 npm 项目
npm init -y

# 安装 Hardhat
npm install --save-dev hardhat

# 初始化 Hardhat 项目
npx hardhat init

选择 "Create a TypeScript project" 选项。

2.2 Hardhat 配置文件

// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-verify";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "hardhat-deploy";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      viaIR: true, // 启用 IR 优化
    },
  },
  networks: {
    hardhat: {
      chainId: 31337,
      forking: {
        url: process.env.MAINNET_RPC_URL || "",
        enabled: process.env.FORKING === "true",
      },
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      chainId: 11155111,
    },
    mainnet: {
      url: process.env.MAINNET_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      chainId: 1,
    },
  },
  gasReporter: {
    enabled: process.env.REPORT_GAS === "true",
    currency: "USD",
    coinmarketcap: process.env.COINMARKETCAP_API_KEY,
    excludeContracts: ["mocks/"],
  },
  etherscan: {
    apiKey: {
      mainnet: process.env.ETHERSCAN_API_KEY || "",
      sepolia: process.env.ETHERSCAN_API_KEY || "",
    },
  },
  paths: {
    sources: "./contracts",
    tests: "./test",
    cache: "./cache",
    artifacts: "./artifacts",
  },
  mocha: {
    timeout: 200000, // 200 seconds
  },
};

export default config;

2.3 环境变量配置

# .env
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=your_private_key_here
ETHERSCAN_API_KEY=your_etherscan_api_key
COINMARKETCAP_API_KEY=your_coinmarketcap_api_key
REPORT_GAS=true
FORKING=false

2.4 示例合约 - ERC20 代币

// contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract TestToken is ERC20, Ownable {
    uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18;

    constructor() ERC20("Test Token", "TEST") Ownable(msg.sender) {
        _mint(msg.sender, 100_000 * 10**18);
    }

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

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

2.5 单元测试示例

// test/Token.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { TestToken } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";

describe("TestToken", function () {
  // Fixture: 部署合约的快照
  async function deployTokenFixture() {
    const [owner, addr1, addr2] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("TestToken");
    const token = await Token.deploy();

    return { token, owner, addr1, addr2 };
  }

  describe("Deployment", function () {
    it("Should set the right owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      expect(await token.owner()).to.equal(owner.address);
    });

    it("Should assign the initial supply to the owner", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const ownerBalance = await token.balanceOf(owner.address);
      expect(ownerBalance).to.equal(ethers.parseEther("100000"));
    });

    it("Should set the correct token name and symbol", async function () {
      const { token } = await loadFixture(deployTokenFixture);
      expect(await token.name()).to.equal("Test Token");
      expect(await token.symbol()).to.equal("TEST");
    });
  });

  describe("Minting", function () {
    it("Should mint tokens to specified address", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const mintAmount = ethers.parseEther("1000");

      await token.mint(addr1.address, mintAmount);
      expect(await token.balanceOf(addr1.address)).to.equal(mintAmount);
    });

    it("Should fail if non-owner tries to mint", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const mintAmount = ethers.parseEther("1000");

      await expect(
        token.connect(addr1).mint(addr1.address, mintAmount)
      ).to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
    });

    it("Should fail if minting exceeds max supply", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const maxSupply = await token.MAX_SUPPLY();
      const currentSupply = await token.totalSupply();
      const excessAmount = maxSupply - currentSupply + 1n;

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

    it("Should emit Transfer event on mint", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const mintAmount = ethers.parseEther("1000");

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

  describe("Burning", function () {
    it("Should burn tokens from caller", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const initialBalance = await token.balanceOf(owner.address);
      const burnAmount = ethers.parseEther("1000");

      await token.burn(burnAmount);
      expect(await token.balanceOf(owner.address))
        .to.equal(initialBalance - burnAmount);
    });

    it("Should fail if burning more than balance", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const burnAmount = ethers.parseEther("1000");

      await expect(
        token.connect(addr1).burn(burnAmount)
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
    });

    it("Should decrease total supply on burn", async function () {
      const { token, owner } = await loadFixture(deployTokenFixture);
      const initialSupply = await token.totalSupply();
      const burnAmount = ethers.parseEther("1000");

      await token.burn(burnAmount);
      expect(await token.totalSupply()).to.equal(initialSupply - burnAmount);
    });
  });

  describe("Transfers", function () {
    it("Should transfer tokens between accounts", async function () {
      const { token, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
      const transferAmount = ethers.parseEther("50");

      // Transfer from owner to addr1
      await token.transfer(addr1.address, transferAmount);
      expect(await token.balanceOf(addr1.address)).to.equal(transferAmount);

      // Transfer from addr1 to addr2
      await token.connect(addr1).transfer(addr2.address, transferAmount);
      expect(await token.balanceOf(addr2.address)).to.equal(transferAmount);
    });

    it("Should fail if sender has insufficient balance", async function () {
      const { token, addr1 } = await loadFixture(deployTokenFixture);
      const transferAmount = ethers.parseEther("1");

      await expect(
        token.connect(addr1).transfer(addr1.address, transferAmount)
      ).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
    });

    it("Should update balances after transfers", async function () {
      const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
      const initialOwnerBalance = await token.balanceOf(owner.address);
      const transferAmount = ethers.parseEther("100");

      await token.transfer(addr1.address, transferAmount);

      expect(await token.balanceOf(owner.address))
        .to.equal(initialOwnerBalance - transferAmount);
      expect(await token.balanceOf(addr1.address)).to.equal(transferAmount);
    });
  });
});

2.6 复杂合约测试 - Staking Pool

// contracts/StakingPool.sol
// 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/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract StakingPool is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardToken;

    uint256 public rewardRate; // 每秒奖励代币数量
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public totalStaked;

    mapping(address => uint256) public stakedBalance;
    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    event Staked(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event RewardPaid(address indexed user, uint256 reward);
    event RewardRateUpdated(uint256 newRate);

    constructor(
        address _stakingToken,
        address _rewardToken,
        uint256 _rewardRate
    ) Ownable(msg.sender) {
        stakingToken = IERC20(_stakingToken);
        rewardToken = IERC20(_rewardToken);
        rewardRate = _rewardRate;
    }

    function rewardPerToken() public view returns (uint256) {
        if (totalStaked == 0) {
            return rewardPerTokenStored;
        }
        return rewardPerTokenStored +
            (((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / totalStaked);
    }

    function earned(address account) public view returns (uint256) {
        return ((stakedBalance[account] *
            (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18) +
            rewards[account];
    }

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        totalStaked += amount;
        stakedBalance[msg.sender] += amount;
        stakingToken.safeTransferFrom(msg.sender, address(this), amount);
        emit Staked(msg.sender, amount);
    }

    function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
        require(amount > 0, "Cannot withdraw 0");
        require(stakedBalance[msg.sender] >= amount, "Insufficient balance");
        totalStaked -= amount;
        stakedBalance[msg.sender] -= amount;
        stakingToken.safeTransfer(msg.sender, amount);
        emit Withdrawn(msg.sender, amount);
    }

    function getReward() public nonReentrant updateReward(msg.sender) {
        uint256 reward = rewards[msg.sender];
        if (reward > 0) {
            rewards[msg.sender] = 0;
            rewardToken.safeTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
    }

    function exit() external {
        withdraw(stakedBalance[msg.sender]);
        getReward();
    }

    function setRewardRate(uint256 _rewardRate) external onlyOwner updateReward(address(0)) {
        rewardRate = _rewardRate;
        emit RewardRateUpdated(_rewardRate);
    }
}
// test/StakingPool.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";
import { TestToken, StakingPool } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";

describe("StakingPool", function () {
  let stakingToken: TestToken;
  let rewardToken: TestToken;
  let stakingPool: StakingPool;
  let owner: SignerWithAddress;
  let user1: SignerWithAddress;
  let user2: SignerWithAddress;

  const REWARD_RATE = ethers.parseEther("100"); // 100 tokens per second
  const INITIAL_BALANCE = ethers.parseEther("10000");

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

    // Deploy tokens
    const Token = await ethers.getContractFactory("TestToken");
    stakingToken = await Token.deploy();
    rewardToken = await Token.deploy();

    // Deploy staking pool
    const StakingPool = await ethers.getContractFactory("StakingPool");
    stakingPool = await StakingPool.deploy(
      await stakingToken.getAddress(),
      await rewardToken.getAddress(),
      REWARD_RATE
    );

    // Mint tokens to users
    await stakingToken.mint(user1.address, INITIAL_BALANCE);
    await stakingToken.mint(user2.address, INITIAL_BALANCE);

    // Mint reward tokens to pool
    await rewardToken.mint(await stakingPool.getAddress(), ethers.parseEther("1000000"));

    // Approve staking pool
    await stakingToken.connect(user1).approve(
      await stakingPool.getAddress(),
      ethers.MaxUint256
    );
    await stakingToken.connect(user2).approve(
      await stakingPool.getAddress(),
      ethers.MaxUint256
    );
  });

  describe("Staking", function () {
    it("Should allow users to stake tokens", async function () {
      const stakeAmount = ethers.parseEther("1000");

      await expect(stakingPool.connect(user1).stake(stakeAmount))
        .to.emit(stakingPool, "Staked")
        .withArgs(user1.address, stakeAmount);

      expect(await stakingPool.stakedBalance(user1.address)).to.equal(stakeAmount);
      expect(await stakingPool.totalStaked()).to.equal(stakeAmount);
    });

    it("Should fail when staking 0 tokens", async function () {
      await expect(
        stakingPool.connect(user1).stake(0)
      ).to.be.revertedWith("Cannot stake 0");
    });

    it("Should fail when staking without approval", async function () {
      const NewToken = await ethers.getContractFactory("TestToken");
      const newToken = await NewToken.deploy();

      const StakingPool = await ethers.getContractFactory("StakingPool");
      const newPool = await StakingPool.deploy(
        await newToken.getAddress(),
        await rewardToken.getAddress(),
        REWARD_RATE
      );

      await newToken.mint(user1.address, INITIAL_BALANCE);

      await expect(
        newPool.connect(user1).stake(ethers.parseEther("1000"))
      ).to.be.reverted;
    });
  });

  describe("Withdrawing", function () {
    beforeEach(async function () {
      await stakingPool.connect(user1).stake(ethers.parseEther("1000"));
    });

    it("Should allow users to withdraw staked tokens", async function () {
      const withdrawAmount = ethers.parseEther("500");
      const initialBalance = await stakingToken.balanceOf(user1.address);

      await expect(stakingPool.connect(user1).withdraw(withdrawAmount))
        .to.emit(stakingPool, "Withdrawn")
        .withArgs(user1.address, withdrawAmount);

      expect(await stakingPool.stakedBalance(user1.address))
        .to.equal(ethers.parseEther("500"));
      expect(await stakingToken.balanceOf(user1.address))
        .to.equal(initialBalance + withdrawAmount);
    });

    it("Should fail when withdrawing more than staked", async function () {
      await expect(
        stakingPool.connect(user1).withdraw(ethers.parseEther("2000"))
      ).to.be.revertedWith("Insufficient balance");
    });

    it("Should fail when withdrawing 0 tokens", async function () {
      await expect(
        stakingPool.connect(user1).withdraw(0)
      ).to.be.revertedWith("Cannot withdraw 0");
    });
  });

  describe("Rewards", function () {
    it("Should calculate rewards correctly for single staker", async function () {
      const stakeAmount = ethers.parseEther("1000");
      await stakingPool.connect(user1).stake(stakeAmount);

      // Fast forward 100 seconds
      await time.increase(100);

      const expectedReward = REWARD_RATE * 100n;
      const earned = await stakingPool.earned(user1.address);

      // Allow small rounding error
      expect(earned).to.be.closeTo(expectedReward, ethers.parseEther("1"));
    });

    it("Should calculate rewards correctly for multiple stakers", async function () {
      await stakingPool.connect(user1).stake(ethers.parseEther("1000"));
      await time.increase(50);
      await stakingPool.connect(user2).stake(ethers.parseEther("1000"));
      await time.increase(50);

      const earned1 = await stakingPool.earned(user1.address);
      const earned2 = await stakingPool.earned(user2.address);

      // User1 earns for 100 seconds with different rates
      // First 50 seconds: full reward rate
      // Next 50 seconds: half reward rate (sharing with user2)
      const expectedEarned1 = REWARD_RATE * 50n + (REWARD_RATE * 50n) / 2n;

      // User2 earns for 50 seconds at half rate
      const expectedEarned2 = (REWARD_RATE * 50n) / 2n;

      expect(earned1).to.be.closeTo(expectedEarned1, ethers.parseEther("10"));
      expect(earned2).to.be.closeTo(expectedEarned2, ethers.parseEther("10"));
    });

    it("Should allow users to claim rewards", async function () {
      await stakingPool.connect(user1).stake(ethers.parseEther("1000"));
      await time.increase(100);

      const earned = await stakingPool.earned(user1.address);
      const initialBalance = await rewardToken.balanceOf(user1.address);

      await expect(stakingPool.connect(user1).getReward())
        .to.emit(stakingPool, "RewardPaid");

      const finalBalance = await rewardToken.balanceOf(user1.address);
      expect(finalBalance - initialBalance).to.be.closeTo(earned, ethers.parseEther("1"));
      expect(await stakingPool.rewards(user1.address)).to.equal(0);
    });

    it("Should handle exit correctly", async function () {
      const stakeAmount = ethers.parseEther("1000");
      await stakingPool.connect(user1).stake(stakeAmount);
      await time.increase(100);

      const initialStakingBalance = await stakingToken.balanceOf(user1.address);
      const initialRewardBalance = await rewardToken.balanceOf(user1.address);

      await stakingPool.connect(user1).exit();

      expect(await stakingPool.stakedBalance(user1.address)).to.equal(0);
      expect(await stakingToken.balanceOf(user1.address))
        .to.equal(initialStakingBalance + stakeAmount);
      expect(await rewardToken.balanceOf(user1.address))
        .to.be.greaterThan(initialRewardBalance);
    });
  });

  describe("Admin Functions", function () {
    it("Should allow owner to update reward rate", async function () {
      const newRate = ethers.parseEther("200");

      await expect(stakingPool.setRewardRate(newRate))
        .to.emit(stakingPool, "RewardRateUpdated")
        .withArgs(newRate);

      expect(await stakingPool.rewardRate()).to.equal(newRate);
    });

    it("Should fail when non-owner tries to update reward rate", async function () {
      await expect(
        stakingPool.connect(user1).setRewardRate(ethers.parseEther("200"))
      ).to.be.revertedWithCustomError(stakingPool, "OwnableUnauthorizedAccount");
    });
  });

  describe("Edge Cases", function () {
    it("Should handle zero total staked correctly", async function () {
      expect(await stakingPool.rewardPerToken()).to.equal(0);
    });

    it("Should handle multiple stakes and withdraws", async function () {
      await stakingPool.connect(user1).stake(ethers.parseEther("1000"));
      await time.increase(50);
      await stakingPool.connect(user1).stake(ethers.parseEther("500"));
      await time.increase(50);
      await stakingPool.connect(user1).withdraw(ethers.parseEther("700"));
      await time.increase(50);

      const earned = await stakingPool.earned(user1.address);
      expect(earned).to.be.greaterThan(0);
    });
  });
});

3. Foundry 测试框架

3.1 Foundry 安装与初始化

# 安装 Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 创建新项目
forge init foundry-defi-protocol
cd foundry-defi-protocol

# 安装依赖
forge install OpenZeppelin/openzeppelin-contracts

3.2 Foundry 配置

# foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.20"
optimizer = true
optimizer_runs = 200
via_ir = true

[profile.ci]
fuzz = { runs = 5000 }
invariant = { runs = 1000 }

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"

[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = { key = "${ETHERSCAN_API_KEY}" }

[fmt]
line_length = 100
tab_width = 4
bracket_spacing = true

3.3 Foundry 测试示例

// test/Token.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract TokenTest is Test {
    TestToken public token;
    address public owner;
    address public user1;
    address public user2;

    uint256 constant INITIAL_SUPPLY = 100_000 * 10**18;
    uint256 constant MAX_SUPPLY = 1_000_000 * 10**18;

    function setUp() public {
        owner = address(this);
        user1 = makeAddr("user1");
        user2 = makeAddr("user2");

        token = new TestToken();
    }

    function test_InitialState() public {
        assertEq(token.name(), "Test Token");
        assertEq(token.symbol(), "TEST");
        assertEq(token.totalSupply(), INITIAL_SUPPLY);
        assertEq(token.balanceOf(owner), INITIAL_SUPPLY);
    }

    function test_Mint() public {
        uint256 mintAmount = 1000 * 10**18;
        token.mint(user1, mintAmount);

        assertEq(token.balanceOf(user1), mintAmount);
        assertEq(token.totalSupply(), INITIAL_SUPPLY + mintAmount);
    }

    function testFail_MintExceedsMaxSupply() public {
        uint256 excessAmount = MAX_SUPPLY - INITIAL_SUPPLY + 1;
        token.mint(user1, excessAmount);
    }

    function testRevert_NonOwnerMint() public {
        vm.prank(user1);
        vm.expectRevert();
        token.mint(user1, 1000 * 10**18);
    }

    function test_Transfer() public {
        uint256 transferAmount = 100 * 10**18;
        token.transfer(user1, transferAmount);

        assertEq(token.balanceOf(user1), transferAmount);
        assertEq(token.balanceOf(owner), INITIAL_SUPPLY - transferAmount);
    }

    function test_Burn() public {
        uint256 burnAmount = 1000 * 10**18;
        uint256 initialSupply = token.totalSupply();

        token.burn(burnAmount);

        assertEq(token.balanceOf(owner), INITIAL_SUPPLY - burnAmount);
        assertEq(token.totalSupply(), initialSupply - burnAmount);
    }

    // Fuzz Testing
    function testFuzz_Mint(address to, uint256 amount) public {
        vm.assume(to != address(0));
        vm.assume(amount > 0 && amount <= MAX_SUPPLY - INITIAL_SUPPLY);

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

    function testFuzz_Transfer(address to, uint256 amount) public {
        vm.assume(to != address(0));
        vm.assume(amount <= INITIAL_SUPPLY);

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

3.4 高级测试 - 模糊测试与不变性测试

// test/StakingPool.invariant.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract StakingPoolHandler is Test {
    StakingPool public pool;
    TestToken public stakingToken;
    TestToken public rewardToken;

    address[] public actors;
    address internal currentActor;

    uint256 public ghost_stakeSum;
    uint256 public ghost_withdrawSum;

    constructor(StakingPool _pool, TestToken _stakingToken, TestToken _rewardToken) {
        pool = _pool;
        stakingToken = _stakingToken;
        rewardToken = _rewardToken;
    }

    function stake(uint256 amount, uint256 actorSeed) public {
        currentActor = actors[bound(actorSeed, 0, actors.length - 1)];
        amount = bound(amount, 1, stakingToken.balanceOf(currentActor));

        vm.startPrank(currentActor);
        stakingToken.approve(address(pool), amount);
        pool.stake(amount);
        vm.stopPrank();

        ghost_stakeSum += amount;
    }

    function withdraw(uint256 amount, uint256 actorSeed) public {
        currentActor = actors[bound(actorSeed, 0, actors.length - 1)];
        amount = bound(amount, 0, pool.stakedBalance(currentActor));

        if (amount == 0) return;

        vm.prank(currentActor);
        pool.withdraw(amount);

        ghost_withdrawSum += amount;
    }

    function getReward(uint256 actorSeed) public {
        currentActor = actors[bound(actorSeed, 0, actors.length - 1)];

        vm.prank(currentActor);
        pool.getReward();
    }

    function addActor(address actor) external {
        actors.push(actor);
    }
}

contract StakingPoolInvariantTest is Test {
    StakingPool public pool;
    TestToken public stakingToken;
    TestToken public rewardToken;
    StakingPoolHandler public handler;

    function setUp() public {
        stakingToken = new TestToken();
        rewardToken = new TestToken();

        pool = new StakingPool(
            address(stakingToken),
            address(rewardToken),
            100 * 10**18
        );

        handler = new StakingPoolHandler(pool, stakingToken, rewardToken);

        // Setup actors
        for (uint i = 0; i < 10; i++) {
            address actor = makeAddr(string(abi.encodePacked("actor", vm.toString(i))));
            handler.addActor(actor);
            stakingToken.mint(actor, 10000 * 10**18);
        }

        // Fund reward pool
        rewardToken.mint(address(pool), 1000000 * 10**18);

        targetContract(address(handler));
    }

    // Invariant: total staked should equal sum of all user balances
    function invariant_TotalStakedEqualsUserBalances() public {
        uint256 sumOfBalances;
        for (uint i = 0; i < 10; i++) {
            address actor = makeAddr(string(abi.encodePacked("actor", vm.toString(i))));
            sumOfBalances += pool.stakedBalance(actor);
        }
        assertEq(pool.totalStaked(), sumOfBalances);
    }

    // Invariant: pool should hold correct amount of staking tokens
    function invariant_PoolHoldsCorrectStakingTokens() public {
        assertEq(
            stakingToken.balanceOf(address(pool)),
            pool.totalStaked()
        );
    }

    // Invariant: ghost accounting should match
    function invariant_GhostAccountingMatches() public {
        assertEq(
            handler.ghost_stakeSum() - handler.ghost_withdrawSum(),
            pool.totalStaked()
        );
    }
}

3.5 Fork 测试

// test/UniswapFork.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract UniswapForkTest is Test {
    IUniswapV2Router02 public router;
    IERC20 public weth;
    IERC20 public usdc;

    address constant ROUTER_ADDRESS = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    address public user;

    function setUp() public {
        // Fork mainnet at specific block
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 18000000);

        router = IUniswapV2Router02(ROUTER_ADDRESS);
        weth = IERC20(WETH_ADDRESS);
        usdc = IERC20(USDC_ADDRESS);

        user = makeAddr("user");
        vm.deal(user, 100 ether);
    }

    function test_SwapETHForUSDC() public {
        uint256 amountIn = 1 ether;
        uint256 minAmountOut = 0;

        address[] memory path = new address[](2);
        path[0] = WETH_ADDRESS;
        path[1] = USDC_ADDRESS;

        vm.startPrank(user);

        uint256[] memory amounts = router.swapExactETHForTokens{value: amountIn}(
            minAmountOut,
            path,
            user,
            block.timestamp + 300
        );

        vm.stopPrank();

        assertGt(usdc.balanceOf(user), 0);
        assertGt(amounts[1], 0);
    }

    function test_GetAmountsOut() public {
        uint256 amountIn = 1 ether;

        address[] memory path = new address[](2);
        path[0] = WETH_ADDRESS;
        path[1] = USDC_ADDRESS;

        uint256[] memory amounts = router.getAmountsOut(amountIn, path);

        assertGt(amounts[1], 0);
        console.log("1 ETH =", amounts[1] / 10**6, "USDC");
    }
}

4. 集成测试

4.1 多合约交互测试

// test/integration/DeFiProtocol.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";

describe("DeFi Protocol Integration", function () {
  let token: any;
  let stakingPool: any;
  let governance: any;
  let owner: any;
  let users: any[];

  before(async function () {
    [owner, ...users] = await ethers.getSigners();

    // Deploy all contracts
    const Token = await ethers.getContractFactory("TestToken");
    token = await Token.deploy();

    const rewardToken = await Token.deploy();

    const StakingPool = await ethers.getContractFactory("StakingPool");
    stakingPool = await StakingPool.deploy(
      await token.getAddress(),
      await rewardToken.getAddress(),
      ethers.parseEther("100")
    );

    // Distribute tokens
    for (const user of users.slice(0, 5)) {
      await token.mint(user.address, ethers.parseEther("10000"));
      await token.connect(user).approve(
        await stakingPool.getAddress(),
        ethers.MaxUint256
      );
    }

    await rewardToken.mint(await stakingPool.getAddress(), ethers.parseEther("1000000"));
  });

  it("Complete user journey: stake -> earn -> withdraw", async function () {
    const user = users[0];
    const stakeAmount = ethers.parseEther("1000");

    // 1. User stakes tokens
    await stakingPool.connect(user).stake(stakeAmount);
    expect(await stakingPool.stakedBalance(user.address)).to.equal(stakeAmount);

    // 2. Time passes and user earns rewards
    await time.increase(3600); // 1 hour
    const earned = await stakingPool.earned(user.address);
    expect(earned).to.be.greaterThan(0);

    // 3. User claims rewards
    await stakingPool.connect(user).getReward();

    // 4. User withdraws stake
    await stakingPool.connect(user).withdraw(stakeAmount);
    expect(await stakingPool.stakedBalance(user.address)).to.equal(0);
  });

  it("Multiple users staking scenario", async function () {
    const stakeAmounts = [
      ethers.parseEther("1000"),
      ethers.parseEther("2000"),
      ethers.parseEther("500"),
    ];

    // Users stake at different times
    for (let i = 0; i < 3; i++) {
      await stakingPool.connect(users[i]).stake(stakeAmounts[i]);
      await time.increase(600); // 10 minutes between stakes
    }

    // Check total staked
    const expectedTotal = stakeAmounts.reduce((a, b) => a + b, 0n);
    expect(await stakingPool.totalStaked()).to.equal(expectedTotal);

    // Verify rewards are distributed proportionally
    await time.increase(3600);

    const rewards = await Promise.all(
      users.slice(0, 3).map(u => stakingPool.earned(u.address))
    );

    // User with 2x stake should have approximately 2x rewards
    expect(rewards[1]).to.be.greaterThan(rewards[0]);
    expect(rewards[0]).to.be.greaterThan(rewards[2]);
  });
});

5. 测试覆盖率

5.1 Hardhat 覆盖率

# 安装覆盖率工具
npm install --save-dev solidity-coverage

# 运行覆盖率测试
npx hardhat coverage
// hardhat.config.js 中已包含
require("solidity-coverage");

生成的覆盖率报告:

-------------------|----------|----------|----------|----------|----------------|
File               |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
-------------------|----------|----------|----------|----------|----------------|
 contracts/        |      100 |    95.83 |      100 |      100 |                |
  StakingPool.sol  |      100 |    95.83 |      100 |      100 |                |
  Token.sol        |      100 |      100 |      100 |      100 |                |
-------------------|----------|----------|----------|----------|----------------|
All files          |      100 |    95.83 |      100 |      100 |                |
-------------------|----------|----------|----------|----------|----------------|

5.2 Foundry 覆盖率

# 运行覆盖率测试
forge coverage

# 生成详细报告
forge coverage --report lcov
genhtml lcov.info -o coverage

6. Gas 优化测试

6.1 Gas Reporter 配置

// hardhat.config.ts 中的 gasReporter 配置
gasReporter: {
  enabled: process.env.REPORT_GAS === "true",
  currency: "USD",
  coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  excludeContracts: ["mocks/"],
  outputFile: "gas-report.txt",
  noColors: true,
}

6.2 Gas 优化示例

优化前:

// 未优化的版本
function batchTransfer(address[] memory recipients, uint256[] memory amounts) external {
    require(recipients.length == amounts.length, "Length mismatch");

    for (uint256 i = 0; i < recipients.length; i++) {
        _transfer(msg.sender, recipients[i], amounts[i]);
    }
}

优化后:

// 优化版本
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
    uint256 length = recipients.length;
    require(length == amounts.length, "Length mismatch");

    for (uint256 i; i < length;) {
        _transfer(msg.sender, recipients[i], amounts[i]);
        unchecked { ++i; }
    }
}

优化对比测试:

describe("Gas Optimization", function () {
  it("Should compare gas costs", async function () {
    const recipients = [addr1.address, addr2.address, addr3.address];
    const amounts = [
      ethers.parseEther("100"),
      ethers.parseEther("200"),
      ethers.parseEther("300")
    ];

    const tx = await token.batchTransfer(recipients, amounts);
    const receipt = await tx.wait();

    console.log("Gas used:", receipt!.gasUsed.toString());
  });
});

7. 部署脚本

7.1 Hardhat 部署脚本

// scripts/deploy.ts
import { ethers, network } from "hardhat";
import { verify } from "./verify";

async function main() {
  console.log("Deploying to network:", network.name);

  const [deployer] = await ethers.getSigners();
  console.log("Deploying with account:", deployer.address);

  const balance = await ethers.provider.getBalance(deployer.address);
  console.log("Account balance:", ethers.formatEther(balance), "ETH");

  // Deploy Token
  console.log("\nDeploying TestToken...");
  const Token = await ethers.getContractFactory("TestToken");
  const token = await Token.deploy();
  await token.waitForDeployment();
  const tokenAddress = await token.getAddress();
  console.log("TestToken deployed to:", tokenAddress);

  // Deploy Reward Token
  console.log("\nDeploying Reward Token...");
  const rewardToken = await Token.deploy();
  await rewardToken.waitForDeployment();
  const rewardTokenAddress = await rewardToken.getAddress();
  console.log("Reward Token deployed to:", rewardTokenAddress);

  // Deploy Staking Pool
  console.log("\nDeploying StakingPool...");
  const rewardRate = ethers.parseEther("100"); // 100 tokens per second
  const StakingPool = await ethers.getContractFactory("StakingPool");
  const stakingPool = await StakingPool.deploy(
    tokenAddress,
    rewardTokenAddress,
    rewardRate
  );
  await stakingPool.waitForDeployment();
  const stakingPoolAddress = await stakingPool.getAddress();
  console.log("StakingPool deployed to:", stakingPoolAddress);

  // Initial setup
  console.log("\nInitial setup...");
  const rewardAmount = ethers.parseEther("1000000");
  await rewardToken.mint(stakingPoolAddress, rewardAmount);
  console.log("Minted", ethers.formatEther(rewardAmount), "reward tokens to pool");

  // Save deployment info
  const deploymentInfo = {
    network: network.name,
    deployer: deployer.address,
    contracts: {
      TestToken: tokenAddress,
      RewardToken: rewardTokenAddress,
      StakingPool: stakingPoolAddress,
    },
    timestamp: new Date().toISOString(),
  };

  const fs = require("fs");
  const path = require("path");
  const deploymentsDir = path.join(__dirname, "../deployments");

  if (!fs.existsSync(deploymentsDir)) {
    fs.mkdirSync(deploymentsDir);
  }

  fs.writeFileSync(
    path.join(deploymentsDir, `${network.name}.json`),
    JSON.stringify(deploymentInfo, null, 2)
  );

  console.log("\nDeployment info saved to:", `deployments/${network.name}.json`);

  // Verify contracts on Etherscan
  if (network.name !== "hardhat" && network.name !== "localhost") {
    console.log("\nWaiting for block confirmations...");
    await token.deploymentTransaction()?.wait(6);

    console.log("\nVerifying contracts on Etherscan...");
    await verify(tokenAddress, []);
    await verify(rewardTokenAddress, []);
    await verify(stakingPoolAddress, [tokenAddress, rewardTokenAddress, rewardRate]);
  }

  console.log("\nDeployment complete!");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

7.2 验证脚本

// scripts/verify.ts
import { run } from "hardhat";

export async function verify(contractAddress: string, args: any[]) {
  console.log("Verifying contract at:", contractAddress);

  try {
    await run("verify:verify", {
      address: contractAddress,
      constructorArguments: args,
    });
    console.log("Contract verified successfully");
  } catch (error: any) {
    if (error.message.toLowerCase().includes("already verified")) {
      console.log("Contract already verified");
    } else {
      console.error("Verification failed:", error);
    }
  }
}

7.3 Foundry 部署脚本

// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/Token.sol";
import "../src/StakingPool.sol";

contract DeployScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployerPrivateKey);

        // Deploy tokens
        TestToken stakingToken = new TestToken();
        console.log("Staking Token deployed at:", address(stakingToken));

        TestToken rewardToken = new TestToken();
        console.log("Reward Token deployed at:", address(rewardToken));

        // Deploy staking pool
        uint256 rewardRate = 100 * 10**18;
        StakingPool pool = new StakingPool(
            address(stakingToken),
            address(rewardToken),
            rewardRate
        );
        console.log("Staking Pool deployed at:", address(pool));

        // Initial setup
        rewardToken.mint(address(pool), 1_000_000 * 10**18);
        console.log("Minted 1M reward tokens to pool");

        vm.stopBroadcast();

        // Save deployment addresses
        string memory deploymentData = string(
            abi.encodePacked(
                "StakingToken=", vm.toString(address(stakingToken)), "\n",
                "RewardToken=", vm.toString(address(rewardToken)), "\n",
                "StakingPool=", vm.toString(address(pool)), "\n"
            )
        );

        vm.writeFile("deployments.txt", deploymentData);
    }
}

运行部署:

# 部署到 Sepolia
forge script script/Deploy.s.sol:DeployScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify

# 部署到主网 (谨慎!)
forge script script/Deploy.s.sol:DeployScript --rpc-url $MAINNET_RPC_URL --broadcast --verify

8. CI/CD 集成

8.1 GitHub Actions 配置

# .github/workflows/test.yml
name: Smart Contract Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
      with:
        submodules: recursive

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

    - name: Install dependencies
      run: npm ci

    - name: Run Hardhat tests
      run: npx hardhat test
      env:
        REPORT_GAS: true

    - name: Run coverage
      run: npx hardhat coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage/lcov.info
        flags: hardhat

    - name: Install Foundry
      uses: foundry-rs/foundry-toolchain@v1

    - name: Run Forge tests
      run: forge test -vvv

    - name: Run Forge coverage
      run: forge coverage

    - name: Check contract sizes
      run: npx hardhat size-contracts

    - name: Run Slither
      uses: crytic/slither-action@v0.3.0
      with:
        node-version: 18

  lint:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

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

    - name: Install dependencies
      run: npm ci

    - name: Run Solhint
      run: npx solhint 'contracts/**/*.sol'

    - name: Run Prettier
      run: npx prettier --check 'contracts/**/*.sol'

    - name: Run ESLint
      run: npx eslint 'test/**/*.ts' 'scripts/**/*.ts'

8.2 部署工作流

# .github/workflows/deploy.yml
name: Deploy Contracts

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy-testnet:
    runs-on: ubuntu-latest
    environment: testnet

    steps:
    - uses: actions/checkout@v3

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

    - name: Install dependencies
      run: npm ci

    - name: Deploy to Sepolia
      run: npx hardhat run scripts/deploy.ts --network sepolia
      env:
        SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
        PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
        ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

    - name: Upload deployment artifacts
      uses: actions/upload-artifact@v3
      with:
        name: deployments
        path: deployments/

  deploy-mainnet:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-testnet
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v3

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

    - name: Install dependencies
      run: npm ci

    - name: Deploy to Mainnet
      run: npx hardhat run scripts/deploy.ts --network mainnet
      env:
        MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
        PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
        ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}

    - name: Create Release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: false

9. 测试最佳实践

9.1 测试组织结构

test/
├── unit/
│   ├── Token.test.ts
│   ├── StakingPool.test.ts
│   └── Governance.test.ts
├── integration/
│   ├── DeFiProtocol.test.ts
│   └── CrossContractInteractions.test.ts
├── fork/
│   ├── UniswapIntegration.test.ts
│   └── AaveIntegration.test.ts
├── fuzzing/
│   └── StakingPool.invariant.t.sol
└── helpers/
    ├── fixtures.ts
    └── utils.ts

9.2 测试辅助工具

// test/helpers/fixtures.ts
import { ethers } from "hardhat";

export async function deployTokenFixture() {
  const [owner, ...users] = await ethers.getSigners();

  const Token = await ethers.getContractFactory("TestToken");
  const token = await Token.deploy();

  return { token, owner, users };
}

export async function deployStakingFixture() {
  const { token, owner, users } = await deployTokenFixture();

  const RewardToken = await ethers.getContractFactory("TestToken");
  const rewardToken = await RewardToken.deploy();

  const StakingPool = await ethers.getContractFactory("StakingPool");
  const stakingPool = await StakingPool.deploy(
    await token.getAddress(),
    await rewardToken.getAddress(),
    ethers.parseEther("100")
  );

  // Setup
  await rewardToken.mint(
    await stakingPool.getAddress(),
    ethers.parseEther("1000000")
  );

  for (const user of users) {
    await token.mint(user.address, ethers.parseEther("10000"));
    await token.connect(user).approve(
      await stakingPool.getAddress(),
      ethers.MaxUint256
    );
  }

  return { token, rewardToken, stakingPool, owner, users };
}
// test/helpers/utils.ts
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";

export async function mineBlocks(count: number) {
  for (let i = 0; i < count; i++) {
    await ethers.provider.send("evm_mine", []);
  }
}

export async function increaseTime(seconds: number) {
  await time.increase(seconds);
}

export async function setNextBlockTimestamp(timestamp: number) {
  await time.setNextBlockTimestamp(timestamp);
}

export function calculateExpectedReward(
  stakedAmount: bigint,
  rewardRate: bigint,
  duration: number
): bigint {
  return (stakedAmount * rewardRate * BigInt(duration)) / ethers.parseEther("1");
}

export async function expectRevert(
  promise: Promise<any>,
  expectedError: string
) {
  try {
    await promise;
    throw new Error("Expected transaction to revert");
  } catch (error: any) {
    if (!error.message.includes(expectedError)) {
      throw error;
    }
  }
}

9.3 快照测试

import { takeSnapshot, SnapshotRestorer } from "@nomicfoundation/hardhat-network-helpers";

describe("StakingPool with Snapshots", function () {
  let snapshot: SnapshotRestorer;

  beforeEach(async function () {
    // Setup
    snapshot = await takeSnapshot();
  });

  afterEach(async function () {
    await snapshot.restore();
  });

  it("Test 1", async function () {
    // This test starts from snapshot
  });

  it("Test 2", async function () {
    // This test also starts from snapshot
  });
});

10. 安全测试

10.1 Slither 静态分析

# 安装 Slither
pip3 install slither-analyzer

# 运行分析
slither .

# 生成报告
slither . --json slither-report.json

# 检查特定问题
slither . --detect reentrancy-eth,uninitialized-state

10.2 Mythril 符号执行

# 安装 Mythril
pip3 install mythril

# 分析合约
myth analyze contracts/StakingPool.sol --solc-json mythril-config.json

10.3 Echidna 模糊测试

# echidna.yaml
testMode: assertion
testLimit: 50000
deployer: "0x30000"
sender: ["0x10000", "0x20000", "0x30000"]
// contracts/test/EchidnaTest.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../StakingPool.sol";

contract EchidnaTest {
    StakingPool pool;
    TestToken stakingToken;
    TestToken rewardToken;

    constructor() {
        stakingToken = new TestToken();
        rewardToken = new TestToken();
        pool = new StakingPool(
            address(stakingToken),
            address(rewardToken),
            100 * 10**18
        );

        stakingToken.mint(address(this), 1000000 * 10**18);
        rewardToken.mint(address(pool), 1000000 * 10**18);
        stakingToken.approve(address(pool), type(uint256).max);
    }

    function echidna_total_staked_equals_balance() public view returns (bool) {
        return pool.totalStaked() == stakingToken.balanceOf(address(pool));
    }

    function echidna_user_stake_lte_total() public view returns (bool) {
        return pool.stakedBalance(address(this)) <= pool.totalStaked();
    }
}

运行:

echidna-test contracts/test/EchidnaTest.sol --contract EchidnaTest --config echidna.yaml

11. 升级测试

11.1 可升级合约测试

// contracts/StakingPoolV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract StakingPoolV2 is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable {
    IERC20 public stakingToken;
    IERC20 public rewardToken;

    uint256 public rewardRate;
    uint256 public lastUpdateTime;
    uint256 public rewardPerTokenStored;
    uint256 public totalStaked;

    // V2 新增: 锁定期
    uint256 public lockDuration;
    mapping(address => uint256) public stakeTimestamp;

    mapping(address => uint256) public stakedBalance;
    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    function initialize(
        address _stakingToken,
        address _rewardToken,
        uint256 _rewardRate
    ) public initializer {
        __Ownable_init(msg.sender);
        __ReentrancyGuard_init();

        stakingToken = IERC20(_stakingToken);
        rewardToken = IERC20(_rewardToken);
        rewardRate = _rewardRate;
        lockDuration = 7 days; // V2 新增
    }

    function setLockDuration(uint256 _duration) external onlyOwner {
        lockDuration = _duration;
    }

    // 重写 withdraw 添加锁定期检查
    function withdraw(uint256 amount) external {
        require(
            block.timestamp >= stakeTimestamp[msg.sender] + lockDuration,
            "Tokens are locked"
        );
        // ... rest of withdraw logic
    }
}
// test/upgrade/StakingPoolUpgrade.test.ts
import { expect } from "chai";
import { ethers, upgrades } from "hardhat";

describe("StakingPool Upgrade", function () {
  it("Should upgrade successfully", async function () {
    const [owner] = await ethers.getSigners();

    // Deploy V1
    const Token = await ethers.getContractFactory("TestToken");
    const stakingToken = await Token.deploy();
    const rewardToken = await Token.deploy();

    const StakingPoolV1 = await ethers.getContractFactory("StakingPool");
    const proxy = await upgrades.deployProxy(
      StakingPoolV1,
      [
        await stakingToken.getAddress(),
        await rewardToken.getAddress(),
        ethers.parseEther("100")
      ],
      { initializer: 'initialize' }
    );

    // Interact with V1
    await stakingToken.mint(owner.address, ethers.parseEther("1000"));
    await stakingToken.approve(await proxy.getAddress(), ethers.MaxUint256);
    await proxy.stake(ethers.parseEther("500"));

    const stakedBefore = await proxy.stakedBalance(owner.address);

    // Upgrade to V2
    const StakingPoolV2 = await ethers.getContractFactory("StakingPoolV2");
    const upgraded = await upgrades.upgradeProxy(
      await proxy.getAddress(),
      StakingPoolV2
    );

    // Verify state is preserved
    expect(await upgraded.stakedBalance(owner.address)).to.equal(stakedBefore);

    // Verify new functionality
    expect(await upgraded.lockDuration()).to.equal(7 * 24 * 3600);

    await upgraded.setLockDuration(14 * 24 * 3600);
    expect(await upgraded.lockDuration()).to.equal(14 * 24 * 3600);
  });
});

12. 性能基准测试

// test/benchmarks/GasBenchmark.test.ts
import { ethers } from "hardhat";

describe("Gas Benchmarks", function () {
  it("Benchmark different operations", async function () {
    const [owner, user] = await ethers.getSigners();

    const Token = await ethers.getContractFactory("TestToken");
    const token = await Token.deploy();

    await token.mint(owner.address, ethers.parseEther("100000"));

    // Benchmark transfer
    const tx1 = await token.transfer(user.address, ethers.parseEther("100"));
    const receipt1 = await tx1.wait();
    console.log("Transfer gas:", receipt1!.gasUsed.toString());

    // Benchmark mint
    const tx2 = await token.mint(user.address, ethers.parseEther("100"));
    const receipt2 = await tx2.wait();
    console.log("Mint gas:", receipt2!.gasUsed.toString());

    // Benchmark burn
    const tx3 = await token.burn(ethers.parseEther("100"));
    const receipt3 = await tx3.wait();
    console.log("Burn gas:", receipt3!.gasUsed.toString());
  });
});
Prev
链下数据与预言机
Next
MEV与交易优化