以太坊 Foundry 與 Hardhat 部署腳手架完整實戰指南:從環境建置到自動化部署

本文為以太坊智慧合約開發者提供 Foundry 與 Hardhat 兩大主流開發框架的完整實戰指南。Foundry 以其卓越執行速度和 Solidity 原生支援聞名;Hardhat 以其豐富生態系統和 TypeScript 整合能力著稱。2025-2026 年,Pectra 升級帶來 EIP-7702 等新特性,本指南涵蓋如何有效利用這些工具應對最新技術變化,包括完整安裝流程、專案初始化設定、智慧合約開發實戰、測試與部署流程、以及 GitHub Actions 自動化部署工作流程。

以太坊 Foundry 與 Hardhat 部署腳手架完整實戰指南:從環境建置到自動化部署

前言

2025-2026 年以太坊生態系統進入 Pectra 升級階段,EIP-7702 的實施為智慧合約開發帶來全新可能性。本文為以太坊智慧合約開發者提供 Foundry 與 Hardhat 兩大主流開發框架的完整實戰指南,涵蓋從環境建置到生產環境自動化部署的全部流程。

Foundry 以其卓越執行速度(比 Truffle 快 100 倍)和原生 Solidity 測試框架聞名;Hardhat 以其豐富的 TypeScript 生態系統和廣泛社區支援著稱。根據 2025 年以太坊開發者調查,68%''' 的專業開發者使用 Hardhat,32%''' 使用 Foundry,而 **24%''' 同時使用兩者'''。

第一章:環境建置與前置準備

1.1 系統需求與依賴

硬體需求:

軟體需求:

# 驗證 Node.js 版本
node --version  # 應顯示 v20.x.x

# 驗證 npm 版本
npm --version   # 應顯示 9.x.x

# 安裝 Rust(Foundry 依賴)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
rustc --version  # 應顯示 1.75+

1.2 Foundrys 安裝與配置

Foundrys 是 Rust 編寫的智慧合約開發框架,由 Paradigm 開發。安装方式:

# 方式一:直接安裝(推薦)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 方式二:從源碼編譯
git clone https://github.com/foundry-rs/foundry.git
cd foundry
cargo install --path ./crates/forge --bins --force
cargo install --path ./crates/cast --bins --force
cargo install --path ./crates/anvil --bins --force

# 驗證安裝
forge --version  # 顯示 forge version x.x.x
cast --version  # 顯示 cast version x.x.x
anvil --version # 顯示 anvil version x.x.x

Foundrys 工具鏈包含三個核心工具:

工具功能使用場景
Forge測試、部署、腳本執行日常開發主力
Cast鏈上互動、ABI 編碼解碼調試與交互
Anvil本地 EVM 節點開發測試

1.3 Hardhat 安裝與配置

# 初始化專案目錄
mkdir my-ethereum-project && cd my-ethereum-project
npm init -y

# 安裝 Hardhat 核心套件
npm install --save-dev hardhat@^2.22.0

# 初始化 Hardhat 專案
npx hardhat init
# 選擇 "Create an advanced sample project"

# 安裝額外依賴
npm install --save-dev @nomicfoundation/hardhat-toolbox@^4.0.0
npm install --save-dev @nomicfoundation/hardhat-ethers@^3.0.0
npm install --save-dev @nomicfoundation/hardhat-verify@^2.0.0

# 安裝熱重載與除錯工具
npm install --save-dev hardhat-deploy hardhat-gas-reporter
npm install --save-dev @openzeppelin/contracts@^5.0.0

1.4 兩種框架的專案結構比較

Foundry 專案結構:

my-foundry-project/
├── src/                    # 合約原始碼
│   ├── Counter.sol
│   └── tokens/
│       └── MyToken.sol
├── test/                   # 測試檔案
│   ├── Counter.t.sol
│   └── integration/
│       └── CrossChain.t.sol
├── script/                 # 部署腳本
│   └── Deploy.s.sol
├── lib/                    # 依賴庫(Git Submodules)
├── foundry.toml           # Foundry 配置
└── .env                   # 環境變數

Hardhat 專案結構:

my-hardhat-project/
├── contracts/             # 合約原始碼
│   ├── Counter.sol
│   └── token/
│       └── MyToken.sol
├── test/                  # 測試檔案(TypeScript)
│   ├── counter.test.ts
│   └── token.test.ts
├── scripts/               # 部署腳本
│   └── deploy.ts
├── deploy/                # Hardhat Deploy 腳本
│   └── 01_deploy_counter.ts
├── hardhat.config.ts      # Hardhat 配置
├── tsconfig.json          # TypeScript 配置
└── package.json

第二章:Foundry 深度實戰

2.1 foundry.toml 完整配置

# foundry.toml

# 預設網路配置
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 200
via_ir = true

# 測試配置
test = "test"
test_contract = "Test"
test_match_path = "test/**/*.t.sol"

# Fuzz 測試配置
fuzz = { runs = 256, max_test_rejects = 65536 }

# 快速部署配置
broadcast = "broadcast"

[profile.ci]
# CI 環境配置:更嚴格的測試
fuzz = { runs = 10000 }
invariant = { runs = 5000 }

[profile.heavy]
# 重型測試配置
fuzz = { runs = 100000 }
invariant = { runs = 10000 }

# 主網分叉配置
[profile.mainnet_fork]
eth_rpc_url = "${MAINNET_RPC_URL}"
fork_block_number = 20500000

# 本地網路配置
[profile.local]
eth_rpc_url = "http://127.0.0.1:8545"

# 遠端網路配置
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
holesky = "${HOLESKY_RPC_URL}"

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

2.2 測試驅動開發實戰

Foundry 的 Solidity 原生測試框架是其最大特色。以下是完整測試範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/contracts/DeFiVault.sol";

contract DeFiVaultTest is Test {
    DeFiVault public vault;
    address public user;
    address public attacker;
    
    // 測試代幣
    MockERC20 public stakingToken;
    MockERC20 public rewardToken;
    
    function setUp() public {
        // 部署測試合約
        stakingToken = new MockERC20("Staking Token", "STK", 18);
        rewardToken = new MockERC20("Reward Token", "RWD", 18);
        
        vault = new DeFiVault(
            address(stakingToken),
            address(rewardToken),
            1000  // 10% APY (basis points)
        );
        
        // 設定測試用戶
        user = makeAddr("user");
        attacker = makeAddr("attacker");
        
        // 初始化代幣供應
        stakingToken.mint(user, 1000 ether);
        stakingToken.mint(attacker, 100 ether);
        rewardToken.mint(address(vault), 100000 ether);
    }
    
    // === 基本功能測試 ===
    
    function testDeposit() public {
        vm.startPrank(user);
        
        stakingToken.approve(address(vault), 100 ether);
        vault.deposit(100 ether);
        
        assertEq(vault.balanceOf(user), 100 ether);
        assertEq(stakingToken.balanceOf(user), 900 ether);
        assertEq(stakingToken.balanceOf(address(vault)), 100 ether);
        
        vm.stopPrank();
    }
    
    function testWithdraw() public {
        // 先存款
        vm.startPrank(user);
        stakingToken.approve(address(vault), 100 ether);
        vault.deposit(100 ether);
        
        // 等一段時間後提款
        vm.warp(block.timestamp + 365 days);
        vault.withdraw(50 ether);
        
        assertEq(vault.balanceOf(user), 50 ether);
        
        vm.stopPrank();
    }
    
    function testRewardAccrual() public {
        vm.startPrank(user);
        
        stakingToken.approve(address(vault), 100 ether);
        vault.deposit(100 ether);
        
        // 時間跳轉 1 年
        vm.warp(block.timestamp + 365 days);
        vault.claimReward();
        
        // 應獲得 10% 收益
        uint256 expectedReward = 10 ether;  // 100 * 10%
        uint256 actualReward = rewardToken.balanceOf(user);
        
        assertGe(actualReward, expectedReward - 1);  // 允許 1 wei 誤差
        
        vm.stopPrank();
    }
    
    // === 邊界條件測試 ===
    
    function testZeroDeposit() public {
        vm.expectRevert("Cannot deposit zero");
        vault.deposit(0);
    }
    
    function testInsufficientBalance() public {
        vm.startPrank(user);
        
        stakingToken.approve(address(vault), 100 ether);
        vault.deposit(50 ether);
        
        vm.expectRevert("Insufficient balance");
        vault.withdraw(100 ether);
        
        vm.stopPrank();
    }
    
    function testEmergencyWithdraw() public {
        vm.startPrank(user);
        
        stakingToken.approve(address(vault), 100 ether);
        vault.deposit(100 ether);
        
        // 緊急提取
        vault.emergencyWithdraw();
        
        assertEq(stakingToken.balanceOf(user), 100 ether);
        assertEq(vault.balanceOf(user), 0);
        
        vm.stopPrank();
    }
    
    // === Fuzz 測試 ===
    
    function testFuzz_DepositAmount(uint256 amount) public {
        vm.assume(amount > 0 && amount <= 1000 ether);
        
        vm.startPrank(user);
        stakingToken.mint(user, amount);
        stakingToken.approve(address(vault), amount);
        
        uint256 balanceBefore = stakingToken.balanceOf(user);
        vault.deposit(amount);
        uint256 balanceAfter = stakingToken.balanceOf(user);
        
        assertEq(balanceBefore - balanceAfter, amount);
        assertEq(vault.balanceOf(user), amount);
        
        vm.stopPrank();
    }
    
    // === 狀態不变量測試 ===
    
    function testInvariant_TotalSupplyConsistency() public {
        // 定義不变量:合約持有的代幣 >= 總質押量
        // 當質押者質押 x,應有:vault.tokenBalance >= sum(vault.balanceOf(all))
    }
    
    // === 效能測試 ===
    
    function testGas_Deposit() public {
        vm.startPrank(user);
        stakingToken.approve(address(vault), type(uint256).max);
        
        uint256 gasStart = gasleft();
        vault.deposit(1 ether);
        uint256 gasUsed = gasStart - gasleft();
        
        console.log("Gas used for deposit:", gasUsed);
        // 通常應低於 100000 gas
        
        vm.stopPrank();
    }
}

2.3 部署腳本實戰

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "forge-std/Script.sol";
import "../src/contracts/DeFiVault.sol";
import "../src/interfaces/IMockERC20.sol";

contract DeployScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);
        
        // 部署 Mock ERC20 代幣
        IMockERC20 stakingToken = IMockERC20(
            new MockERC20("Staking Token", "STK", 18)
        );
        
        IMockERC20 rewardToken = IMockERC20(
            new MockERC20("Reward Token", "RWD", 18)
        );
        
        // 部署主要合約
        DeFiVault vault = new DeFiVault(
            address(stakingToken),
            address(rewardToken),
            1000  // 10% APY
        );
        
        // 轉帳初始獎勵代幣到 Vault
        rewardToken.mint(address(vault), 1000000 ether);
        
        vm.stopBroadcast();
        
        // 輸出部署資訊
        console.log("DeFiVault deployed to:", address(vault));
        console.log("StakingToken deployed to:", address(stakingToken));
        console.log("RewardToken deployed to:", address(rewardToken));
    }
}

2.4 Cast 指令實戰

# 查看合約函數簽名
cast sig "transfer(address,uint256)"
# 輸出:0xa9059cbb

# 查詢鏈上數據
cast balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
# 輸出:1234567890123456789

# 查詢代幣餘額
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
    "balanceOf(address)(uint256)" \
    0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

# 估算 Gas
cast estimate 0xContractAddress \
    "deposit(uint256)" 1000000000000000000 \
    --from 0xYourAddress

# 發送交易
cast send 0xContractAddress \
    "deposit(uint256)" 1000000000000000000 \
    --private-key 0xYourPrivateKey

# 解析事件日誌
cast parse-log /path/to/event.log

第三章:Hardhat 深度實戰

3.1 hardhat.config.ts 完整配置

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@nomicfoundation/hardhat-ethers";
import "@nomicfoundation/hardhat-verify";
import "hardhat-deploy";
import "hardhat-gas-reporter";
import "solidity-coverage";
import * as dotenv from "dotenv";

dotenv.config();

const PRIVATE_KEY = process.env.PRIVATE_KEY || "0x0000000000000000000000000000000000000000000000000000000000000000";
const MAINNET_RPC_URL = process.env.MAINNET_RPC_URL || "https://eth.llamarpc.com";
const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org";
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.26",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      viaIR: true,
    },
  },
  
  networks: {
    hardhat: {
      chainId: 31337,
      forking: {
        url: MAINNET_RPC_URL,
        blockNumber: 20500000,  // 特定區塊分叉
        enabled: process.env.FORK_ENABLED === "true",
      },
      accounts: {
        mnemonic: "test test test test test test test test test test test junk",
        count: 20,
      },
    },
    
    localhost: {
      url: "http://127.0.0.1:8545",
      chainId: 31337,
    },
    
    mainnet: {
      url: MAINNET_RPC_URL,
      chainId: 1,
      accounts: [PRIVATE_KEY],
      gasPrice: "auto",
    },
    
    sepolia: {
      url: SEPOLIA_RPC_URL,
      chainId: 11155111,
      accounts: [PRIVATE_KEY],
      gasPrice: "auto",
    },
    
    holesky: {
      url: process.env.HOLESKY_RPC_URL || "",
      chainId: 17000,
      accounts: [PRIVATE_KEY],
      gasPrice: "auto",
    },
  },
  
  etherscan: {
    apiKey: {
      mainnet: ETHERSCAN_API_KEY,
      sepolia: ETHERSCAN_API_KEY,
    },
    customChains: [
      {
        network: "holesky",
        chainId: 17000,
        urls: {
          apiURL: "https://api-holesky.etherscan.io/api",
          browserURL: "https://holesky.etherscan.io",
        },
      },
    ],
  },
  
  gasReporter: {
    enabled: process.env.REPORT_GAS === "true",
    currency: "USD",
    token: "ETH",
    gasPriceApi: "https://api.etherscan.io/api?module=gastacker&action=gasoracle",
    coinmarketcap: process.env.COINMARKETCAP_API_KEY,
  },
  
  paths: {
    sources: "./contracts",
    tests: "./test",
    cache: "./cache",
    artifacts: "./artifacts",
    deploy: "./deploy",
  },
  
  namedAccounts: {
    deployer: {
      default: 0,
    },
    governance: {
      mainnet: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52",
    },
  },
  
  typechain: {
    outDir: "./typechain-types",
    target: "ethers-v6",
  },
};

export default config;

3.2 TypeScript 測試實戰

import { expect } from "chai";
import { ethers, network, deployments } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { DeFiVault, MockERC20 } from "../typechain-types";

describe("DeFiVault", () => {
  let vault: DeFiVault;
  let stakingToken: MockERC20;
  let rewardToken: MockERC20;
  let owner: SignerWithAddress;
  let user: SignerWithAddress;
  let attacker: SignerWithAddress;

  beforeEach(async () => {
    [owner, user, attacker] = await ethers.getSigners();
    
    // 部署測試合約
    const StakingToken = await ethers.getContractFactory("MockERC20");
    stakingToken = (await StakingToken.deploy(
      "Staking Token",
      "STK",
      18
    )) as MockERC20;
    await stakingToken.waitForDeployment();

    const RewardToken = await ethers.getContractFactory("MockERC20");
    rewardToken = (await RewardToken.deploy(
      "Reward Token",
      "RWD",
      18
    )) as MockERC20;
    await rewardToken.waitForDeployment();

    const Vault = await ethers.getContractFactory("DeFiVault");
    vault = (await Vault.deploy(
      await stakingToken.getAddress(),
      await rewardToken.getAddress(),
      1000  // 10% APY
    )) as DeFiVault;
    await vault.waitForDeployment();

    // 初始化測試代幣
    const mintAmount = ethers.parseEther("1000");
    await stakingToken.mint(user.address, mintAmount);
    await stakingToken.mint(attacker.address, ethers.parseEther("100"));
    await rewardToken.mint(await vault.getAddress(), ethers.parseEther("100000"));

    // 授權
    await stakingToken.connect(user).approve(
      await vault.getAddress(),
      ethers.MaxUint256
    );
  });

  describe("Deposit", () => {
    it("should accept deposits and update balances", async () => {
      const depositAmount = ethers.parseEther("100");
      
      await expect(vault.connect(user).deposit(depositAmount))
        .to.emit(vault, "Deposited")
        .withArgs(user.address, depositAmount);

      expect(await vault.balanceOf(user.address)).to.equal(depositAmount);
    });

    it("should reject zero deposits", async () => {
      await expect(
        vault.connect(user).deposit(0)
      ).to.be.revertedWithCustomError(vault, "ZeroAmount");
    });

    it("should reject deposits exceeding balance", async () => {
      const balance = await stakingToken.balanceOf(user.address);
      await expect(
        vault.connect(user).deposit(balance + 1n)
      ).to.be.revertedWithCustomError(vault, "InsufficientBalance");
    });
  });

  describe("Reward Distribution", () => {
    it("should accrue rewards over time", async () => {
      const depositAmount = ethers.parseEther("100");
      await vault.connect(user).deposit(depositAmount);

      // 時間跳轉 1 年
      await network.provider.send("evm_increaseTime", [365 * 24 * 60 * 60]);
      await network.provider.send("evm_mine");

      // 領取獎勵
      await vault.connect(user).claimReward();
      
      const rewardBalance = await rewardToken.balanceOf(user.address);
      const expectedMinReward = ethers.parseEther("9");  // 略低於 10% 理論值
      
      expect(rewardBalance).to.be.gte(expectedMinReward);
    });
  });

  describe("Security", () => {
    it("should prevent reentrancy attacks", async () => {
      const depositAmount = ethers.parseEther("100");
      
      // 部署攻擊合約測試
      const AttackerFactory = await ethers.getContractFactory("ReentrancyAttacker");
      const attackerContract = await AttackerFactory.deploy(
        await vault.getAddress(),
        await stakingToken.getAddress()
      );
      
      // 給攻擊合約一些代幣
      await stakingToken.transfer(
        await attackerContract.getAddress(),
        depositAmount
      );

      // 嘗試攻擊
      await expect(
        attackerContract.connect(attacker).attack(depositAmount)
      ).to.be.reverted;
    });
  });

  describe("Integration", () => {
    it("should work with mainnet fork", async () => {
      // 僅在分叉模式下運行
      if (!process.env.FORK_ENABLED) {
        console.log("Skipping mainnet fork test");
        return;
      }

      // 在主網分叉上測試與真實代幣的交互
      const usdtAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
      const usdt = await ethers.getContractAt("IERC20", usdtAddress);
      
      const balance = await usdt.balanceOf(owner.address);
      console.log(`USDT Balance: ${ethers.formatUnits(balance, 6)}`);
    });
  });
});

3.3 Hardhat Deploy 部署腳本

// deploy/01_deploy_tokens.ts
import { HardhatRuntimeEnvironment } from "hardhat/types";

module.exports = async function (hre: HardhatRuntimeEnvironment) {
  const { deployments, getNamedAccounts } = hre;
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();

  log("Deploying Mock ERC20 Tokens...");

  // 部署質押代幣
  const stakingToken = await deploy("StakingToken", {
    from: deployer,
    args: ["Staking Token", "STK", 18],
    log: true,
    waitConfirmations: 6,
  });

  // 部署獎勵代幣
  const rewardToken = await deploy("RewardToken", {
    from: deployer,
    args: ["Reward Token", "RWD", 18],
    log: true,
    waitConfirmations: 6,
  });

  log(`StakingToken deployed to: ${stakingToken.address}`);
  log(`RewardToken deployed to: ${rewardToken.address}`);
};

module.exports.tags = ["tokens", "mocks"];
module.exports.dependencies = [];
// deploy/02_deploy_vault.ts
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { ethers } from "hardhat";

module.exports = async function (hre: HardhatRuntimeEnvironment) {
  const { deployments, getNamedAccounts, network } = hre;
  const { deploy, log, execute } = deployments;
  const { deployer, governance } = await getNamedAccounts();

  const stakingToken = await ethers.getContract("StakingToken");
  const rewardToken = await ethers.getContract("RewardToken");

  log("Deploying DeFi Vault...");

  const vault = await deploy("DeFiVault", {
    from: deployer,
    args: [
      await stakingToken.getAddress(),
      await rewardToken.getAddress(),
      1000  // 10% APY in basis points
    ],
    log: true,
    waitConfirmations: network.name === "mainnet" ? 6 : 1,
  });

  log(`DeFiVault deployed to: ${vault.address}`);

  // 設置治理角色
  if (network.name !== "hardhat" && network.name !== "localhost") {
    try {
      await execute(
        "DeFiVault",
        { from: deployer, log: true },
        "grantRole",
        ethers.keccak256(ethers.toUtf8Bytes("GOVERNANCE_ROLE")),
        governance
      );
    } catch (e) {
      console.log("Governance role setup might have failed");
    }
  }

  // 驗證源碼
  if (network.name !== "hardhat" && network.name !== "localhost") {
    try {
      await hre.run("verify:verify", {
        address: vault.address,
        constructorArguments: [
          await stakingToken.getAddress(),
          await rewardToken.getAddress(),
          1000
        ],
      });
      log("Contract verified on Etherscan");
    } catch (e) {
      log("Verification failed (might already be verified)");
    }
  }
};

module.exports.tags = ["vault", "core"];
module.exports.dependencies = ["tokens"];

第四章:GitHub Actions 自動化部署

4.1 Foundry CI/CD 工作流

# .github/workflows/test-foundry.yml
name: Foundry Tests

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

env:
  FOUNDRY_PROFILE: ci

jobs:
  test:
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest]
    
    runs-on: ${{ matrix.os }}
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          submodules: recursive
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm install
      
      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
      
      - name: Run Forge build
        run: forge build
        env:
          FOUNDRY_PROFILE: ci
      
      - name: Run Forge tests
        run: forge test
        env:
          FOUNDRY_PROFILE: ci
          # Fork mainnet for integration tests
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
      
      - name: Run fuzz tests
        run: forge test --fuzz-runs 10000
        env:
          FOUNDRY_PROFILE: ci
      
      - name: Upload coverage reports
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.lcov
          flags: unittests

  deployment:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      
      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
      
      - name: Load environment variables
        run: cp .env.example .env
        env:
          PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
          ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
      
      - name: Deploy to Sepolia (testnet)
        run: |
          forge script script/Deploy.s.sol:DeployScript \
            --rpc-url sepolia \
            --broadcast \
            --verify \
            --private-key ${{ secrets.DEPLOYER_PRIVATE_KEY }}
        env:
          SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
      
      - name: Deploy to Mainnet
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: |
          forge script script/Deploy.s.sol:DeployScript \
            --rpc-url mainnet \
            --broadcast \
            --verify \
            --private-key ${{ secrets.DEPLOYER_PRIVATE_KEY }}
            --slow
        env:
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}

4.2 Hardhat CI/CD 工作流

# .github/workflows/test-hardhat.yml
name: Hardhat Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Compile contracts
        run: npx hardhat compile
      
      - name: Run tests
        run: npx hardhat test
        env:
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
      
      - name: Run coverage
        run: npx hardhat coverage
        env:
          COINMARKETCAP_API_KEY: ${{ secrets.COINMARKETCAP_API_KEY }}
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
      
      - name: Run Slither static analysis
        uses: crytic/slither-action@v0.3.0
        with:
          slither-config: slither.config.json
          fail-on: medium

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Deploy to Sepolia
        run: npx hardhat deploy --network sepolia
        env:
          PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
          ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
          SEPOLIA_RPC_URL: ${{ secrets.SEPOLIA_RPC_URL }}
      
      - name: Deploy to Mainnet
        run: npx hardhat deploy --network mainnet
        env:
          PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
          ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
          MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}

第五章:框架比較與選擇策略

5.1 詳細功能對比

功能FoundryHardhat
測試速度比 Truffle 快 100 倍快 10-20 倍
測試語言Solidity 原生TypeScript/JavaScript
Fuzzing原生支持需要 Pact/Hypothesis
主網分叉forge test --fork-urlHardhat Network 分叉
調試器forge debugHardhat Network GDebug
部署腳本SolidityTypeScript
插件生態較少但核心豐富多元
TypeChain需要手動整合原生支持
VS Code 整合vscfoundry 擴展Hardhat VSCode

5.2 推薦使用場景

選擇 Foundry:

選擇 Hardhat:

兩者同時使用:

# 典型配置:Foundry 測試 + Hardhat 部署
/project
├── contracts/        # Solidity 合約
├── foundry.toml     # Foundry 配置
├── hardhat.config.ts # Hardhat 配置
├── test/            # Foundry 測試 (.t.sol)
├── scripts/         # Foundry 腳本 (.s.sol)
└── deploy/          # Hardhat Deploy 腳本

第六章:進階開發技巧

6.1 使用 Anvil 進行本地開發

# 啟動 Anvil 節點(可分叉主網)
anvil --fork-url $MAINNET_RPC_URL \
      --fork-block-number 20500000 \
      --chain-id 31337

# 使用 Cast 與 Anvil 交互
cast balance 0x... --rpc-url http://localhost:8545

# 快照與恢復狀態
anvil snapshot
anvil mine --timestamp 1234567890

6.2 使用 Hardhat Network 調試

// 在測試中使用 console.log
import "hardhat/console.sol";

// 進階調試配置
// hardhat.config.ts
{
  networks: {
    hardhat: {
      logging: {
        enable: true,
      },
    },
  },
}

// 使用區塊瀏覽器
// 在 Hardhat Network 模式下打開 http://localhost:5173

6.3 EIP-7702 智能合約錢包部署

// 使用 EIP-7702 的智能合約錢包部署示例
// 注意:需要 Pectra 升級後才能使用

const walletImplementation = await ethers.deployContract("SmartContractWallet");
const factory = await ethers.getContract("MinimalAccountFactory");

const userOp = {
  sender: walletAddress,
  nonce: await factory.getNonce(walletAddress),
  initCode: hexZeroPad(walletImplementation.address, 20),
  callData: walletInterface.encodeFunctionData("initialize", [owner]),
  callGasLimit: 500000,
  verificationGasLimit: 500000,
  preVerificationGas: 21000,
  maxFeePerGas: ethers.parseUnits("30", "gwei"),
  maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"),
};

結語

Foundry 與 Hardhat 各有優勢,選擇應根據團隊技術背景和項目需求。建議:

無論選擇哪個框架,確保:

  1. 完整測試覆蓋(單元測試、集成測試、Fuzz 測試)
  2. 第三方安全審計
  3. 漸進式部署策略(先測試網後主網)
  4. 完善的監控和應急響應機制

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!