以太坊 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 系統需求與依賴
硬體需求:
- CPU:至少 4 核心(建議 8 核心以上)
- 記憶體:16GB RAM(建議 32GB)
- 儲存:100GB SSD(建議 NVMe SSD)
軟體需求:
- Node.js 20.x LTS 或更高版本
- npm 9.x 或 yarn 1.22+
- Rust 1.75+(僅 Foundry 需要)
# 驗證 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 詳細功能對比
| 功能 | Foundry | Hardhat |
|---|---|---|
| 測試速度 | 比 Truffle 快 100 倍 | 快 10-20 倍 |
| 測試語言 | Solidity 原生 | TypeScript/JavaScript |
| Fuzzing | 原生支持 | 需要 Pact/Hypothesis |
| 主網分叉 | forge test --fork-url | Hardhat Network 分叉 |
| 調試器 | forge debug | Hardhat Network GDebug |
| 部署腳本 | Solidity | TypeScript |
| 插件生態 | 較少但核心 | 豐富多元 |
| TypeChain | 需要手動整合 | 原生支持 |
| VS Code 整合 | vscfoundry 擴展 | Hardhat VSCode |
5.2 推薦使用場景
選擇 Foundry:
- 需要極速測試迴圈(TDD)
- 大量使用 Fuzzing 測試
- 專注於 Solidity 智能合約開發
- 需要形式化驗證整合
- 熟悉 Solidity 生態
選擇 Hardhat:
- 需要 TypeScript/JavaScript 生態整合
- 複雜的前後端整合項目
- 需要豐富的插件支援
- 團隊成員熟悉 Node.js 生態
- 需要 Hardhat Network 調試功能
兩者同時使用:
# 典型配置: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 各有優勢,選擇應根據團隊技術背景和項目需求。建議:
- 小型獨立項目或追求測試速度:優先使用 Foundry
- 大型企業級項目或有前端整合需求:使用 Hardhat
- 最佳實踐:熟悉兩者,根據場景靈活切換
無論選擇哪個框架,確保:
- 完整測試覆蓋(單元測試、集成測試、Fuzz 測試)
- 第三方安全審計
- 漸進式部署策略(先測試網後主網)
- 完善的監控和應急響應機制
相關文章
- 以太坊開發者完整學習路徑:從 Solidity 基礎到智能合約安全大師 — 本文專為軟體開發者設計系統化的以太坊學習路徑,涵蓋區塊鏈基礎理論、Solidity 智能合約開發、以太坊開發工具生態、Layer 2 開發、DeFi 協議實現、以及智能合約安全審計等核心主題。從工程師視角出發,提供可直接應用於實際項目的技術內容,包括完整的程式碼範例和開發環境配置。
- 以太坊開發者本地環境建置完整指南:從零到部署的實戰教學 — 本指南提供從零開始建置以太坊開發環境的完整實戰流程,涵蓋 Hardhat、Foundry 兩大主流開發框架架設、本地區塊鏈節點架設、以及智慧合約部署至測試網的完整環節。包含完整的程式碼範例,從專案初始化、合約撰寫、測試撰寫到部署的每個步驟都有詳細說明。
- 以太坊智能合約部署完整實戰指南:從環境建置到主網上線 — 本文提供以太坊智能合約部署的完整實戰教學,涵蓋從開發環境建置、本地測試、測試網部署、到主網上線的全部流程。我們以實際的 ERC-20 代幣合約為例,逐步講解每個環節的技術細節、可能遇到的問題、以及最佳實踐建議,幫助開發者掌握智能合約部署的核心技能。
- 以太坊智能合約開發實戰完整教程:從環境建置到部署的工程實踐 — 本指南從工程師視角出發,提供完整的智能合約開發實戰教學。內容涵蓋主流開發框架 Hardhat 與 Foundry 的深度使用、測試驅動開發實踐、模糊測試工具應用、以及生產環境部署的最佳實踐,幫助開發者建立完善的智能合約開發工作流。
- Solidity Debug 實戰指南:智能合約除錯技術與常見錯誤完整解析 — 智能合約開發中的除錯是一項極具挑戰性的任務。與傳統應用程式不同,智能合約一旦部署便無法修改,任何錯誤都可能導致不可逆轉的資產損失。本文從實戰角度出發,提供完整的 Solidity 智能合約除錯方法論,涵蓋 Remix IDE 的視覺化除錯工具、Hardhat 和 Foundry 的本地測試網除錯能力、EVM 字节码層級的分析技巧、以及 20 種最常見智能合約錯誤的診斷與修復方案。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!