Foundry 完整專案開發實戰教程:從零構建 DeFi 借貸協議
本文提供一個完整的 Foundry 開發框架實戰教程,通過構建一個去中心化借貸協議的完整過程,深入介紹 Foundry 在真實項目開發中的應用。涵蓋專案初始化、智慧合約開發、完整測試覆蓋(單元測試、模糊測試、不變量測試)、部署腳本編寫等完整流程。
Foundry 完整專案開發實戰教程:從零構建 DeFi 借貸協議
概述
本文提供一個完整的 Foundry 開發框架實戰教程,通過構建一個去中心化借貸協議的完整過程,深入介紹 Foundry 在真實項目開發中的應用。我們將從專案初始化開始,涵蓋智慧合約開發、完整測試覆蓋、部署腳本編寫、以及自動化測試部署的完整流程。
本教程的目標讀者是具有一定 Solidity 基礎的開發者,通過本教程後讀者將能夠獨立使用 Foundry 構建完整的 DeFi 項目。整個項目將包含以下核心功能:存款、借款、清算、利率模型、以及完整的安全防護機制。
一、專案初始化與結構設計
1.1 使用 Foundry 創建新項目
Foundry 提供了便捷的項目初始化工具,可以快速創建標準化的智能合約項目結構:
# 使用 foundryup 安裝 Foundry
curl -L https://foundry.paradigm.xyz | bash
export PATH="$HOME/.foundry/bin:$PATH"
# 初始化新項目
forge init lending-protocol
cd lending-protocol
# 查看項目結構
tree .
執行後的項目結構如下:
lending-protocol/
├── lib/ # 依賴庫
├── script/ # 部署腳本
├── src/ # 合約源代碼
├── test/ # 測試檔案
├── foundry.toml # Foundry 配置
└── README.md
1.2 配置 Foundry 環境
Foundry 的配置文件 foundry.toml 控制著編譯、測試、部署的各項參數:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.28"
# 測試配置
test = "test"
cache_path = "cache"
# 優化器配置
optimizer = true
optimizer_runs = 200
via_ir = true
# 詳細輸出配置
verbosity = 2
# RPC 配置
eth_rpc_url = "${ETH_RPC_URL}"
etherscan_api_key = "${ETHERSCAN_API_KEY}"
[profile.ci]
# CI 環境配置
fuzz_runs = 1000
invariant_runs = 256
1.3 安裝必要依賴
對於借貸協議項目,我們需要安裝以下核心依賴:
# 安裝 OpenZeppelin 合約庫
forge install OpenZeppelin/openzeppelin-contracts@v5.0.0 --no-commit
# 安裝 Solmate 庫(高性能合約組件)
forge install Transmissions11/solmate --no-commit
# 安裝 Foundry 開發工具
forge install foundry-rs/forge-std --no-commit
安裝完成後的 lib 目錄結構:
lib/
├── forge-std/
├── openzeppelin-contracts/
└── solmate/
二、借貸協議核心合約設計
2.1 合約架構概述
我們的借貸協議採用模組化設計,主要包含以下核心合約:
合約架構圖
═══════════════════════════════════════════════════════════════════
┌─────────────────────┐
│ LendingProtocol │ 主合約入口
└──────────┬──────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ InterestRate │ │ Liquidation │ │ Oracle │
│ Model │ │ Engine │ │ Interface │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
└───────────────────┼───────────────────┘
│
┌──────────┴──────────┐
│ Market Config │ 市場配置
└─────────────────────┘
═══════════════════════════════════════════════════════════════════
2.2 利率模型合約
利率模型是借貸協議的核心組件,直接影響資金的供給和需求平衡。我們實現一個分段線性利率模型:
// src/InterestRateModel.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
/**
* @title InterestRateModel
* @notice 分段線性利率模型
* @dev 採用「 Utilization Rate 」驅動的利率曲線
*/
abstract contract InterestRateModel {
/// @notice 基礎利率(當利用率為 0 時)
uint256 public immutable baseRatePerSecond;
/// @notice 最佳利用率時的斜率
uint256 public immutable slopePerSecond;
/// @notice 最佳利用率(kink point)
uint256 public immutable optimalUtilization;
/// @notice 超過最佳利用率後的額外斜率
uint256 public immutable extraSlopePerSecond;
/**
* @param _baseRatePerSecond 年化基礎利率(以 1e18 為底)
* @param _optimalUtilization 最佳利用率(以 1e18 為底,如 80% = 8e17)
* @param _slopePerSecond 達到最佳利用率前的斜率
* @param _extraSlopePerSecond 超過最佳利用率後的斜率
*/
constructor(
uint256 _baseRatePerSecond,
uint256 _optimalUtilization,
uint256 _slopePerSecond,
uint256 _extraSlopePerSecond
) {
require(_optimalUtilization <= 1e18, "invalid optimal utilization");
baseRatePerSecond = _baseRatePerSecond;
optimalUtilization = _optimalUtilization;
slopePerSecond = _slopePerSecond;
extraSlopePerSecond = _extraSlopePerSecond;
}
/**
* @notice 計算借款利率
* @param utilization 當前資產利用率
* @return borrowRatePerSecond 每秒借款利率
*/
function getBorrowRate(uint256 utilization) public view returns (uint256) {
if (utilization <= optimalUtilization) {
// 線性區間
return baseRatePerSecond +
(utilization * slopePerSecond) / optimalUtilization;
} else {
// 超過最佳利用率區間
uint256 optimalRate = baseRatePerSecond + slopePerSecond;
uint256 excessUtilization = utilization - optimalUtilization;
uint256 excessRate = (excessUtilization * extraSlopePerSecond) /
(1e18 - optimalUtilization);
return optimalRate + excessRate;
}
}
/**
* @notice 計算存款利率
* @param totalBorrows 總借款金額
* @param totalReserves 總儲備金
* @param utilization 利用率
* @return supplyRatePerSecond 每秒存款利率
*/
function getSupplyRate(
uint256 totalBorrows,
uint256 totalReserves,
uint256 utilization
) public view returns (uint256) {
if (totalBorrows == 0) return 0;
uint256 borrowRate = getBorrowRate(utilization);
uint256 reserveFactor = 0.1e18; // 10% 儲備金
// 存款利率 = 借款利率 × (1 - 儲備因子) × 利用率
return (borrowRate * (1e18 - reserveFactor) * utilization) / 1e36;
}
}
2.3 市場合約實現
市場合約是借貸協議的核心,負責處理存款、借款和清算:
// src/Market.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {InterestRateModel} from "./InterestRateModel.sol";
/**
* @title Market
* @notice 借貸市場合約
* @dev 支持存款、借款、利率計算功能
*/
contract Market {
using SafeERC20 for IERC20;
using Math for uint256;
// ============ 常量 ============
uint256 public constant SECONDS_PER_YEAR = 365 days;
uint256 public constant LIQUIDATION_THRESHOLD = 0.85e18; // 85% 清算門檻
uint256 public constant LIQUIDATION_BONUS = 1.05e18; // 5% 清算獎金
// ============ 狀態變量 ============
IERC20 public immutable underlying;
InterestRateModel public immutable interestRateModel;
// 借款總額
uint256 public totalBorrows;
// 儲備金
uint256 public totalReserves;
// 存款總額(浮動)
uint256 public totalCash;
// 抵押品價值
uint256 public totalCollateral;
// 用戶借款記錄
mapping(address => uint256) public borrowBalance;
// 用戶存款記錄
mapping(address => uint256) public depositBalance;
// 用戶抵押品
mapping(address => uint256) public collateralAmount;
// 用戶是否已有借款
mapping(address => bool) public hasBorrowed;
// 最後更新時間
uint256 public lastUpdateTimestamp;
// 借款利率累加器
uint256 public borrowIndex;
// ============ 事件 ============
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event Borrow(address indexed user, uint256 amount);
event Repay(address indexed user, uint256 amount);
event Liquidate(
address indexed liquidator,
address indexed borrower,
uint256 repayAmount,
uint256 seizedAmount
);
event AccrueInterest(uint256 interestAccumulated, uint256 newIndex);
// ============ 修飾符 ============
modifier updateInterest() {
_accrueInterest();
_;
}
/**
* @param _underlying 底層資產代幣
* @param _interestRateModel 利率模型合約
*/
constructor(address _underlying, address _interestRateModel) {
require(_underlying != address(0), "invalid underlying");
require(_interestRateModel != address(0), "invalid rate model");
underlying = IERC20(_underlying);
interestRateModel = InterestRateModel(_interestRateModel);
lastUpdateTimestamp = block.timestamp;
borrowIndex = 1e18;
}
// ============ 存款功能 ============
/**
* @notice 存款
* @param amount 存款金額
*/
function deposit(uint256 amount) external updateInterest {
require(amount > 0, "cannot deposit 0");
underlying.safeTransferFrom(msg.sender, address(this), amount);
depositBalance[msg.sender] += amount;
totalCash += amount;
emit Deposit(msg.sender, amount);
}
/**
* @notice 提款
* @param amount 提款金額
*/
function withdraw(uint256 amount) external updateInterest {
require(amount > 0, "cannot withdraw 0");
require(depositBalance[msg.sender] >= amount, "insufficient balance");
depositBalance[msg.sender] -= amount;
totalCash -= amount;
underlying.safeTransfer(msg.sender, amount);
// 檢查提款後健康因子
_requireHealthy(msg.sender);
emit Withdraw(msg.sender, amount);
}
// ============ 借款功能 ============
/**
* @notice 借款
* @param amount 借款金額
*/
function borrow(uint256 amount) external updateInterest {
require(amount > 0, "cannot borrow 0");
require(totalCash >= amount, "insufficient liquidity");
// 計算借款人應累積的利息
uint256 borrowerIndex = borrowIndex;
uint256 interest = _calculateInterest(msg.sender, borrowerIndex);
// 更新借款餘額
if (!hasBorrowed[msg.sender]) {
hasBorrowed[msg.sender] = true;
}
borrowBalance[msg.sender += interest;
// 檢查抵押品是否足夠
require(
collateralAmount[msg.sender] * LIQUIDATION_THRESHOLD >=
(borrowBalance[msg.sender] + amount),
"insufficient collateral"
);
borrowBalance[msg.sender] += amount;
totalBorrows += amount;
totalCash -= amount;
underlying.safeTransfer(msg.sender, amount);
emit Borrow(msg.sender, amount);
}
/**
* @notice 還款
* @param amount 還款金額
*/
function repay(uint256 amount) external updateInterest {
require(amount > 0, "cannot repay 0");
require(hasBorrowed[msg.sender], "no outstanding debt");
uint256 borrowerIndex = borrowIndex;
uint256 owed = _calculateInterest(msg.sender, borrowerIndex);
uint256 actualRepay = amount > owed ? owed : amount;
borrowBalance[msg.sender] -= actualRepay;
totalBorrows -= actualRepay;
totalCash += actualRepay;
underlying.safeTransferFrom(msg.sender, address(this), actualRepay);
if (borrowBalance[msg.sender] == 0) {
hasBorrowed[msg.sender] = false;
}
emit Repay(msg.sender, actualRepay);
}
// ============ 抵押品功能 ============
/**
* @notice 添加抵押品
* @param amount 抵押品金額
*/
function addCollateral(uint256 amount) external updateInterest {
require(amount > 0, "cannot add 0 collateral");
underlying.safeTransferFrom(msg.sender, address(this), amount);
collateralAmount[msg.sender] += amount;
totalCollateral += amount;
}
// ============ 清算功能 ============
/**
* @notice 清算
* @param borrower 借款人地址
* @param repayAmount 償還金額
*/
function liquidate(
address borrower,
uint256 repayAmount
) external updateInterest {
require(hasBorrowed[borrower], "no debt to liquidate");
// 計算健康因子
uint256 health = _getHealthFactor(borrower);
require(health < 1e18, "account healthy");
// 計算應償還和可獲取的抵押品
uint256 borrowerIndex = borrowIndex;
uint256 owed = _calculateInterest(borrower, borrowerIndex);
uint256 actualRepay = repayAmount > owed ? owed : repayAmount;
// 計算清算獎勵
uint256 seizeAmount = (actualRepay * LIQUIDATION_BONUS) / 1e18;
require(
collateralAmount[borrower] >= seizeAmount,
"insufficient collateral"
);
// 更新狀態
borrowBalance[borrower] -= actualRepay;
collateralAmount[borrower] -= seizeAmount;
totalBorrows -= actualRepay;
totalCollateral -= seizeAmount;
// 轉帳
underlying.safeTransferFrom(
msg.sender,
address(this),
actualRepay
);
underlying.safeTransfer(msg.sender, seizeAmount);
emit Liquidate(msg.sender, borrower, actualRepay, seizeAmount);
}
// ============ 內部函數 ============
/**
* @notice 累積利息
*/
function _accrueInterest() internal {
uint256 currentTimestamp = block.timestamp;
uint256 timeDelta = currentTimestamp - lastUpdateTimestamp;
if (timeDelta == 0) return;
uint256 utilization = totalBorrows > 0
? (totalBorrows * 1e18) / (totalCash + totalBorrows - totalReserves)
: 0;
uint256 borrowRate = interestRateModel.getBorrowRate(utilization);
uint256 interest = (totalBorrows * borrowRate * timeDelta) / 1e18;
totalBorrows += interest;
borrowIndex += (interest * 1e18) / totalBorrows;
lastUpdateTimestamp = currentTimestamp;
emit AccrueInterest(interest, borrowIndex);
}
/**
* @notice 計算用戶利息
*/
function _calculateInterest(
address user,
uint256 currentIndex
) internal view returns (uint256) {
if (!hasBorrowed[user]) return 0;
uint256 storedIndex = userBorrowIndex[user];
if (storedIndex == 0) storedIndex = 1e18;
uint256 indexDelta = currentIndex - storedIndex;
return (borrowBalance[user] * indexDelta) / 1e18;
}
/**
* @notice 獲取健康因子
*/
function _getHealthFactor(address user) internal view returns (uint256) {
if (borrowBalance[user] == 0) return type(uint256).max;
return (collateralAmount[user] * LIQUIDATION_THRESHOLD) /
borrowBalance[user];
}
/**
* @notice 檢查健康狀態
*/
function _requireHealthy(address user) internal view {
require(_getHealthFactor(user) >= 1e18, "account unhealthy");
}
// ============ 查詢函數 ============
/**
* @notice 獲取帳戶健康因子
*/
function getHealthFactor(address user) external view returns (uint256) {
return _getHealthFactor(user);
}
/**
* @notice 獲取帳戶借款餘額(含利息)
*/
function getBorrowBalance(address user) external view returns (uint256) {
if (!hasBorrowed[user]) return 0;
return borrowBalance[user] + _calculateInterest(user, borrowIndex);
}
/**
* @notice 獲取當前利用率
*/
function getUtilization() external view returns (uint256) {
if (totalBorrows == 0) return 0;
return (totalBorrows * 1e18) / (totalCash + totalBorrows);
}
/**
* @notice 獲取當前借款利率
*/
function getBorrowRate() external view returns (uint256) {
uint256 utilization = getUtilization();
return interestRateModel.getBorrowRate(utilization);
}
/**
* @notice 獲取當前存款利率
*/
function getSupplyRate() external view returns (uint256) {
uint256 utilization = getUtilization();
return interestRateModel.getSupplyRate(
totalBorrows,
totalReserves,
utilization
);
}
// 用戶借款索引映射
mapping(address => uint256) public userBorrowIndex;
}
三、完整測試套件開發
3.1 基礎單元測試
使用 Foundry 的 Forge 測試框架編寫完整測試:
// test/Market.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Market} from "../src/Market.sol";
import {FixedInterestRateModel} from "../src/FixedInterestRateModel.sol";
/**
* @title MockToken
* @notice 測試用 ERC20 代幣
*/
contract MockToken is ERC20 {
uint8 private _decimals;
constructor(
string memory name,
string memory symbol,
uint8 decimals_
) ERC20(name, symbol) {
_decimals = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimals;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}
/**
* @title MarketTest
* @notice Market 合約完整測試套件
*/
contract MarketTest is Test {
Market public market;
MockToken public underlying;
FixedInterestRateModel public interestModel;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
address public liquidator = makeAddr("liquidator");
uint256 constant INITIAL_AMOUNT = 1000e18;
function setUp() public {
// 部署測試代幣
underlying = new MockToken("Test Token", "TEST", 18);
// 部署利率模型(年化 5% 基礎利率,80% 最佳利用率)
interestModel = new FixedInterestRateModel(
0.00000000158187689e18, // ~5% 年化
0.8e18, // 80% 最佳利用率
0.00000000474563068e18, // ~15% 斜率
0.0000000158187689e18 // 超過後斜率
);
// 部署市場合約
market = new Market(address(underlying), address(interestModel));
// 給測試帳戶充值
underlying.mint(alice, INITIAL_AMOUNT * 10);
underlying.mint(bob, INITIAL_AMOUNT * 10);
underlying.mint(liquidator, INITIAL_AMOUNT * 10);
// 授權市場合約
vm.prank(alice);
underlying.approve(address(market), type(uint256).max);
vm.prank(bob);
underlying.approve(address(market), type(uint256).max);
vm.prank(liquidator);
underlying.approve(address(market), type(uint256).max);
}
// ============ 存款測試 ============
function test_Deposit() public {
uint256 depositAmount = 100e18;
vm.prank(alice);
market.deposit(depositAmount);
assertEq(market.depositBalance(alice), depositAmount);
assertEq(underlying.balanceOf(address(market)), depositAmount);
}
function test_DepositZero() public {
vm.prank(alice);
vm.expectRevert("cannot deposit 0");
market.deposit(0);
}
function test_MultipleDeposits() public {
vm.prank(alice);
market.deposit(100e18);
vm.prank(alice);
market.deposit(200e18);
assertEq(market.depositBalance(alice), 300e18);
}
// ============ 提款測試 ============
function test_Withdraw() public {
uint256 depositAmount = 100e18;
uint256 withdrawAmount = 50e18;
vm.prank(alice);
market.deposit(depositAmount);
uint256 balanceBefore = underlying.balanceOf(alice);
vm.prank(alice);
market.withdraw(withdrawAmount);
assertEq(market.depositBalance(alice), depositAmount - withdrawAmount);
assertEq(
underlying.balanceOf(alice),
balanceBefore + withdrawAmount - depositAmount
);
}
function test_WithdrawMoreThanDeposited() public {
vm.prank(alice);
market.deposit(100e18);
vm.prank(alice);
vm.expectRevert("insufficient balance");
market.withdraw(150e18);
}
// ============ 借款測試 ============
function test_Borrow() public {
uint256 depositAmount = 100e18;
uint256 borrowAmount = 50e18;
// Alice 存款
vm.prank(alice);
market.deposit(depositAmount);
// Alice 添加抵押品
vm.prank(alice);
market.addCollateral(depositAmount);
// Alice 借款
vm.prank(alice);
market.borrow(borrowAmount);
assertEq(market.borrowBalance(alice), borrowAmount);
}
function test_BorrowWithoutCollateral() public {
vm.prank(alice);
market.deposit(100e18);
vm.prank(alice);
vm.expectRevert("insufficient collateral");
market.borrow(50e18);
}
function test_BorrowMoreThanCollateral() public {
vm.prank(alice);
market.deposit(100e18);
vm.prank(alice);
market.addCollateral(50e18); // 只添加 50 作為抵押品
vm.prank(alice);
vm.expectRevert("insufficient collateral");
market.borrow(60e18); // 超過抵押品價值
}
// ============ 還款測試 ============
function test_Repay() public {
uint256 depositAmount = 100e18;
uint256 borrowAmount = 50e18;
vm.prank(alice);
market.deposit(depositAmount);
vm.prank(alice);
market.addCollateral(depositAmount);
vm.prank(alice);
market.borrow(borrowAmount);
// 還款
vm.prank(alice);
market.repay(borrowAmount);
assertEq(market.borrowBalance(alice), 0);
}
// ============ 利率測試 ============
function test_InterestAccrual() public {
uint256 depositAmount = 1000e18;
uint256 borrowAmount = 500e18;
vm.prank(alice);
market.deposit(depositAmount);
vm.prank(alice);
market.addCollateral(depositAmount);
vm.prank(alice);
market.borrow(borrowAmount);
// 模擬時間流逝(1 年)
vm.warp(block.timestamp + 365 days);
vm.roll(block.number + 365 * 720); // 每 12 秒一個區塊
// 觸發利息累積
vm.prank(alice);
market.deposit(1); // 任何操作都會觸發利息計算
uint256 newBorrowBalance = market.borrowBalance(alice);
assertGt(newBorrowBalance, borrowAmount);
}
// ============ 清算測試 ============
function test_Liquidate() public {
uint256 aliceDeposit = 200e18;
uint256 aliceBorrow = 100e18;
// Alice 存款並借款
vm.prank(alice);
market.deposit(aliceDeposit);
vm.prank(alice);
market.addCollateral(aliceDeposit);
vm.prank(alice);
market.borrow(aliceBorrow);
// 模擬抵押品價值下跌(時間流逝導致利息累積)
vm.warp(block.timestamp + 200 days);
vm.roll(block.number + 200 * 720);
vm.prank(alice);
market.deposit(1); // 觸發利息計算
// 檢查健康因子
uint256 health = market.getHealthFactor(alice);
console.log("Health factor:", health);
// Bob 作為清算人
uint256 bobBalanceBefore = underlying.balanceOf(bob);
vm.prank(bob);
market.liquidate(alice, market.borrowBalance(alice));
// 驗證清算結果
assertGt(underlying.balanceOf(bob), bobBalanceBefore);
}
function test_CannotLiquidateHealthyAccount() public {
vm.prank(alice);
market.deposit(100e18);
vm.prank(alice);
market.addCollateral(100e18);
vm.prank(alice);
market.borrow(50e18); // 健康借款
vm.prank(bob);
vm.expectRevert("account healthy");
market.liquidate(alice, 50e18);
}
// ============ 邊界條件測試 ============
function test_UtilizationCalculation() public {
vm.prank(alice);
market.deposit(100e18);
uint256 utilization = market.getUtilization();
assertEq(utilization, 0);
}
function test_ZeroBorrowRate() public {
uint256 rate = market.getBorrowRate();
console.log("Borrow rate:", rate);
assertGt(rate, 0);
}
}
3.2 模糊測試(Fuzz Testing)
Foundry 的模糊測試功能可以自動發現邊界條件漏洞:
// test/MarketFuzz.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {Market} from "../src/Market.sol";
import {FixedInterestRateModel} from "../src/FixedInterestRateModel.sol";
import {MockToken} from "./Market.t.sol";
/**
* @title MarketFuzzTest
* @notice Market 合約模糊測試套件
*/
contract MarketFuzzTest is Test {
Market public market;
MockToken public underlying;
FixedInterestRateModel public interestModel;
address public user = makeAddr("user");
function setUp() public {
underlying = new MockToken("Test", "TST", 18);
interestModel = new FixedInterestRateModel(
0.00000000158187689e18,
0.8e18,
0.00000000474563068e18,
0.0000000158187689e18
);
market = new Market(address(underlying), address(interestModel));
underlying.mint(user, 1e30); // 大量代幣
vm.prank(user);
underlying.approve(address(market), type(uint256).max);
}
/**
* @notice 模糊測試存款功能
*/
function testFuzz_Deposit(uint256 amount) public {
vm.assume(amount > 0 && amount <= 1e30);
vm.prank(user);
market.deposit(amount);
assertEq(market.depositBalance(user), amount);
}
/**
* @notice 模糊測試借款功能
*/
function testFuzz_BorrowWithCollateral(uint256 collateral, uint256 borrow) public {
vm.assume(collateral > 0 && collateral <= 1e30);
vm.assume(borrow > 0 && borrow <= 1e30);
vm.assume(borrow <= collateral * 85 / 100); // 確保健康借款
vm.prank(user);
market.deposit(collateral);
vm.prank(user);
market.addCollateral(collateral);
vm.prank(user);
market.borrow(borrow);
assertEq(market.borrowBalance(user), borrow);
}
/**
* @notice 模糊測試時間跳躍
*/
function testFuzz_TimeWarp(uint256 timeDelta) public {
vm.assume(timeDelta > 0 && timeDelta <= 365 days * 10);
vm.prank(user);
market.deposit(100e18);
vm.prank(user);
market.addCollateral(100e18);
vm.prank(user);
market.borrow(50e18);
vm.warp(block.timestamp + timeDelta);
vm.prank(user);
market.deposit(1); // 觸發利息計算
uint256 balance = market.borrowBalance(user);
assertGt(balance, 50e18);
}
/**
* @notice 模糊測試多次存款
*/
function testFuzz_MultipleDeposits(uint256[10] memory amounts) public {
uint256 total = 0;
for (uint i = 0; i < 10; i++) {
vm.assume(amounts[i] <= 1e30);
if (amounts[i] == 0) continue;
vm.prank(user);
market.deposit(amounts[i]);
total += amounts[i];
}
assertEq(market.depositBalance(user), total);
}
/**
* @notice 模糊測試清算
*/
function testFuzz_Liquidation(
uint256 collateral,
uint256 borrow,
uint256 timeDelta
) public {
vm.assume(collateral > 100 && collateral <= 1e30);
vm.assume(borrow > 10 && borrow < collateral);
vm.assume(timeDelta > 0 && timeDelta <= 365 days);
vm.prank(user);
market.deposit(collateral);
vm.prank(user);
market.addCollateral(collateral);
vm.prank(user);
market.borrow(borrow);
// 時間跳躍導致利息累積
vm.warp(block.timestamp + timeDelta);
vm.prank(user);
market.deposit(1);
// 嘗試清算
uint256 health = market.getHealthFactor(user);
if (health < 1e18) {
vm.prank(user);
market.repay(borrow);
}
// 確保狀態一致
assertGe(market.getHealthFactor(user), 0);
}
}
3.3 不變量測試(Invariant Testing)
不變量測試可以確保合約的關鍵屬性在所有操作下都保持成立:
// test/MarketInvariant.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
import {Handler} from "./Handler.sol";
import {Market} from "../src/Market.sol";
import {FixedInterestRateModel} from "../src/FixedInterestRateModel.sol";
import {MockToken} from "./Market.t.sol";
/**
* @title MarketInvariantTest
* @notice Market 合約不變量測試
*/
contract MarketInvariantTest is Test {
Market public market;
MockToken public underlying;
FixedInterestRateModel public interestModel;
Handler public handler;
function setUp() public {
underlying = new MockToken("Test", "TST", 18);
interestModel = new FixedInterestRateModel(
0.00000000158187689e18,
0.8e18,
0.00000000474563068e18,
0.0000000158187689e18
);
market = new Market(address(underlying), address(interestModel));
// 部署 Handler
handler = new Handler(market, underlying);
// 設置不變量目標
targetContract(address(handler));
// 配置 handler 行為
bytes4[] memory selectors = new bytes4[](5);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.withdraw.selector;
selectors[2] = Handler.borrow.selector;
selectors[3] = Handler.repay.selector;
selectors[4] = Handler.addCollateral.selector;
fuzzSelector.setFuzzSelector(0, selectors);
}
/**
* @notice 總借款不能超過總流動性
*/
function invariant_BorrowsCannotExceedCash() public view {
assertLe(market.totalBorrows(), market.totalCash() + market.totalReserves());
}
/**
* @notice 用戶借款餘額不能為負
*/
function invariant_BorrowBalanceNonNegative() public view {
// 這個測試需要從 handler 獲取所有借款人
}
/**
* @notice 抵押品總額不能減少
*/
function invariant_CollateralCannotGoNegative() public view {
assertGe(market.totalCollateral(), 0);
}
/**
* @notice 健康因子必須有效
*/
function invariant_HealthFactorValid() public view {
address[] memory users = handler.getActiveUsers();
for (uint i = 0; i < users.length; i++) {
uint256 health = market.getHealthFactor(users[i]);
if (market.hasBorrowed(users[i])) {
assertGe(health, 0);
}
}
}
}
/**
* @title Handler
* @notice 用於不變量測試的操作處理器
*/
contract Handler {
Market public market;
MockToken public underlying;
address[] public activeUsers;
mapping(address => bool) public isActive;
uint256 public ghost_depositSum;
uint256 public ghost_withdrawSum;
uint256 public ghost_borrowSum;
uint256 public ghost_repaySum;
constructor(Market _market, MockToken _underlying) {
market = _market;
underlying = _underlying;
underlying.mint(address(this), 1e30);
underlying.approve(address(market), type(uint256).max);
}
function deposit(uint256 amount) public {
amount = bound(amount, 0, 1e30);
if (amount == 0) return;
market.deposit(amount);
ghost_depositSum += amount;
if (!isActive[msg.sender]) {
isActive[msg.sender] = true;
activeUsers.push(msg.sender);
}
}
function withdraw(uint256 amount) public {
uint256 balance = market.depositBalance(msg.sender);
amount = bound(amount, 0, balance);
if (amount == 0) return;
market.withdraw(amount);
ghost_withdrawSum += amount;
}
function borrow(uint256 amount) public {
uint256 maxBorrow = market.depositBalance(msg.sender) * 85 / 100;
amount = bound(amount, 0, maxBorrow);
if (amount == 0) return;
market.borrow(amount);
ghost_borrowSum += amount;
if (!isActive[msg.sender]) {
isActive[msg.sender] = true;
activeUsers.push(msg.sender);
}
}
function repay(uint256 amount) public {
uint256 balance = market.borrowBalance(msg.sender);
amount = bound(amount, 0, balance);
if (amount == 0) return;
market.repay(amount);
ghost_repaySum += amount;
}
function addCollateral(uint256 amount) public {
amount = bound(amount, 0, 1e30);
if (amount == 0) return;
market.addCollateral(amount);
if (!isActive[msg.sender]) {
isActive[msg.sender] = true;
activeUsers.push(msg.sender);
}
}
function getActiveUsers() external view returns (address[] memory) {
return activeUsers;
}
}
四、部署腳本開發
4.1 本地部署腳本
// script/DeployLocal.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script, console} from "forge-std/Script.sol";
import {MockToken} from "../test/Market.t.sol";
import {Market} from "../src/Market.sol";
import {FixedInterestRateModel} from "../src/FixedInterestRateModel.sol";
/**
* @title DeployLocal
* @notice 本地網絡部署腳本
*/
contract DeployLocal is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 部署測試代幣
MockToken underlying = new MockToken(
"Test USDC",
"USDC",
18
);
console.log("Underlying token deployed at:", address(underlying));
// 部署利率模型
FixedInterestRateModel interestModel = new FixedInterestRateModel(
0.00000000158187689e18, // 5% 年化
0.8e18, // 80% 最佳利用率
0.00000000474563068e18, // 15% 斜率
0.0000000158187689e18 // 超過後斜率
);
console.log("Interest rate model deployed at:", address(interestModel));
// 部署市場合約
Market market = new Market(
address(underlying),
address(interestModel)
);
console.log("Market deployed at:", address(market));
vm.stopBroadcast();
console.log("========================================");
console.log("Deployment Summary:");
console.log("Underlying:", address(underlying));
console.log("InterestModel:", address(interestModel));
console.log("Market:", address(market));
console.log("========================================");
}
}
4.2 主網部署腳本
// script/DeployMainnet.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script, console} from "forge-std/Script.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Market} from "../src/Market.sol";
import {FixedInterestRateModel} from "../src/FixedInterestRateModel.sol";
/**
* @title DeployMainnet
* @notice 主網部署腳本
*/
contract DeployMainnet is Script {
// 主網配置
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
function run() external {
uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 部署 ETH 市場
{
FixedInterestRateModel ethRateModel = new FixedInterestRateModel(
0.0000000022835853e18, // 7.2% 年化基礎利率
0.8e18, // 80% 最佳利用率
0.0000000068507558e18, // 21.6% 斜率
0.0000000171268895e18 // 超過後斜率
);
console.log("ETH Interest Rate Model:", address(ethRateModel));
Market ethMarket = new Market(WETH, address(ethRateModel));
console.log("ETH Market:", address(ethMarket));
}
// 部署 USDC 市場
{
FixedInterestRateModel usdcRateModel = new FixedInterestRateModel(
0.00000000158187689e18, // 5% 年化基礎利率
0.8e18, // 80% 最佳利用率
0.00000000474563068e18, // 15% 斜率
0.0000000158187689e18 // 超過後斜率
);
console.log("USDC Interest Rate Model:", address(usdcRateModel));
Market usdcMarket = new Market(USDC, address(usdcRateModel));
console.log("USDC Market:", address(usdcMarket));
}
vm.stopBroadcast();
}
}
五、執行測試與部署
5.1 執行完整測試
# 執行所有測試
forge test
# 執行特定測試
forge test --match-test test_Deposit
# 執行模糊測試
forge test --fuzz-runs 10000
# 執行不變量測試
forge test --invariant-runs 1000
# 產生測試覆蓋率報告
forge coverage
5.2 部署到本地測試網
# 啟動本地節點(Anvil)
anvil
# 部署到本地網絡
forge script script/DeployLocal.s.sol --broadcast --rpc-url http://localhost:8545
5.3 部署到測試網
# 部署到 Sepolia
forge script script/DeployLocal.s.sol --broadcast --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY
# 驗證合約
forge verify-contract <CONTRACT_ADDRESS> src/Market.sol:Market --etherscan-api-key $ETHERSCAN_API_KEY --chain sepolia
總結
本文提供了一個完整的 Foundry 開發框架實戰教程,涵蓋了從項目初始化、核心合約開發、完整測試套件編寫、到部署腳本開發的全部流程。通過本教程,讀者應該能夠:
- 熟練使用 Foundry 框架進行智能合約開發
- 理解 DeFi 借貸協議的核心機制
- 掌握 Foundry 的測試框架,包括單元測試、模糊測試和不變量測試
- 能夠編寫完整的部署腳本並部署到不同網絡
Foundry 的高性能和豐富的測試功能使其成為以太坊智能合約開發的首選工具,熟練掌握 Foundry 將大幅提升開發效率和代碼質量。
相關文章
- 以太坊 AI 代理與 DePIN 整合開發完整指南:從理論架構到實際部署 — 人工智慧與區塊鏈技術的融合正在重塑數位基礎設施的格局。本文深入探討 AI 代理與 DePIN 在以太坊上的整合開發,提供完整的智慧合約程式碼範例,涵蓋 AI 代理控制框架、DePIN 資源協調、自動化 DeFi 交易等實戰應用,幫助開發者快速掌握這項前沿技術。
- ERC-4626 Tokenized Vault 完整實現指南:從標準規範到生產級合約 — 本文深入探討 ERC-4626 標準的技術細節,提供完整的生產級合約實現。內容涵蓋標準接口定義、資產與份額轉換的數學模型、收益策略整合、費用機制設計,並提供可直接部署的 Solidity 代碼範例。通過本指南,開發者可以構建安全可靠的代幣化 vault 系統。
- 以太坊智能合約開發實戰:從基礎到 DeFi 協議完整代碼範例指南 — 本文提供以太坊智能合約開發的完整實戰指南,透過可直接運行的 Solidity 代碼範例,幫助開發者從理論走向實踐。內容涵蓋基礎合約開發、借貸協議實作、AMM 機制實現、以及中文圈特有的應用場景(台灣交易所整合、香港監管合規、Singapore MAS 牌照申請)。本指南假設讀者具備基本的程式設計基礎,熟悉 JavaScript 或 Python 等語言,並對區塊鏈概念有基本理解。
- 以太坊生態應用案例實作完整指南:DeFi、質押、借貸與錢包交互 — 本文提供以太坊生態系統中最常見應用場景的完整實作範例,涵蓋去中心化金融操作、質押服務、智慧合約部署、錢包管理和跨鏈交互等多個維度。所有範例均基於 2026 年第一季度最新的協議版本,並包含可直接運行的程式碼和詳細的操作流程說明。
- 以太坊零知識證明 DeFi 實戰程式碼指南:從電路設計到智慧合約整合 — 本文聚焦於零知識證明在以太坊 DeFi 應用中的實際程式碼實現,從電路編寫到合約部署,從隱私借貸到隱私交易,提供可運行的程式碼範例和詳細的實現說明。涵蓋 Circom、Noir 開發框架、抵押率驗證電路、隱私交易電路、Solidity 驗證合約與 Gas 優化策略。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!