SC Day 79
复习 — Week 13 总结 (Mini Lending 进度 + Solana 项目回顾)
### 1. Week 13 知识图谱
2026-06-28
第四阶段:综合实战 (73-80)reviewmini-lendingsolanatestingsecurityweek13
日期: 2026-06-28 方向: Solidity / Solana / 多链 阶段: 第四阶段:综合实战 (73-80) 标签: #review #mini-lending #solana #testing #security #week13
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 回顾 Week 13 所有知识点,建立知识关联 |
| 实操 | Mini Lending 进度检查 + 测试策略制定 + 安全审查 |
| 产出 | Week 13 知识图谱 + Mini Lending 测试计划 + 安全检查清单 |
核心概念
1. Week 13 知识图谱
Week 13 覆盖范围
├── Day 71: Move 安全
│ ├── 线性类型系统 → 防重入
│ ├── Capability 模式 → 编译时权限
│ ├── Hot Potato → 闪电贷安全
│ └── 剩余攻击面 → 逻辑/经济/权限
│
├── Day 72: DVDF #5 & #8
│ ├── 闪电贷作为攻击放大器
│ ├── 奖励分配操纵 → 时间加权防护
│ ├── 预言机操纵 → Chainlink 替代 spot price
│ └── 五种闪电贷攻击模式
│
├── Day 73: Mini Lending 设计
│ ├── 协议规格文档
│ ├── Aave/Compound 架构对比
│ ├── 核心数据结构
│ └── 接口定义
│
├── Day 74: Solana 安全
│ ├── 6 大漏洞类型
│ ├── Anchor 自动防护
│ ├── 整数溢出 (release 模式!)
│ └── 安全 Vault 示例
│
├── Day 75: Mini Lending 存款池
│ ├── 份额制会计 (ERC4626)
│ ├── 通胀攻击防护
│ ├── 利率模型实现
│ └── 利息累计机制
│
├── Day 76: Solana 性能
│ ├── Zero-Copy 反序列化
│ ├── Compute Units 优化
│ ├── Sealevel 并行模型
│ └── 数据架构设计影响吞吐量
│
├── Day 77: Mini Lending 借款
│ ├── 借款索引机制
│ ├── 健康因子计算
│ ├── Chainlink 集成
│ └── 清算级联风险
│
└── Day 78: Anchor 高级
├── remaining_accounts
├── 自定义错误码
├── 事件日志
└── 增强 Escrow 程序
2. Mini Lending 进度检查
已完成功能
| 功能 | 状态 | Day | 核心合约/函数 |
|---|---|---|---|
| 协议设计与架构 | 已完成 | 73 | 数据结构定义、接口设计 |
| 利率模型 | 已完成 | 75 | InterestRateModel.sol |
| 存款 (supply) | 已完成 | 75 | MiniLendingPool.supply() |
| 取款 (withdraw) | 已完成 | 75 | MiniLendingPool.withdraw() |
| 利息累计 | 已完成 | 75 | _accrueInterest() |
| 份额转换 | 已完成 | 75 | _convertToShares/Assets() |
| 借款 (borrow) | 已完成 | 77 | MiniLendingPool.borrow() |
| 还款 (repay) | 已完成 | 77 | MiniLendingPool.repay() |
| 健康因子 | 已完成 | 77 | _calculateHealthFactor() |
| Chainlink 集成 | 已完成 | 77 | _getAssetPriceUSD() |
| 清算 (liquidate) | 待实现 | 80 | Day 80 实现 |
待实现功能清单
Day 80 计划:
├── liquidate() 函数
│ ├── 健康因子检查 (HF < 1)
│ ├── 部分清算逻辑
│ ├── 清算奖励计算
│ └── 坏账处理
├── 完整测试套件
│ ├── 单元测试
│ ├── 集成测试
│ └── 边界测试
└── Gas 优化(可选)
3. Mini Lending 测试策略
测试金字塔
┌────────┐
│ E2E │ Forked Mainnet 测试
┌┴────────┴┐
│ 集成测试 │ 多合约交互
┌┴──────────┴┐
│ 单元测试 │ 单个函数
┌┴────────────┴┐
│ 静态分析 │ Slither/Mythril
└──────────────┘
单元测试清单
// ============ InterestRateModel 测试 ============
contract InterestRateModelTest is Test {
// 基础利率测试
function test_BorrowRate_ZeroUtilization() public {
// U = 0% → rate = baseRate
uint256 rate = model.getBorrowRatePerYear(0);
assertEq(rate, 2e16); // 2%
}
function test_BorrowRate_OptimalUtilization() public {
// U = 80% → rate = baseRate + slope1 = 2% + 4% = 6%
uint256 rate = model.getBorrowRatePerYear(80e16);
assertEq(rate, 6e16);
}
function test_BorrowRate_MaxUtilization() public {
// U = 100% → rate = baseRate + slope1 + slope2 = 2% + 4% + 300% = 306%
uint256 rate = model.getBorrowRatePerYear(100e16);
assertEq(rate, 306e16);
}
// 边界测试
function test_BorrowRate_JustAboveOptimal() public {
uint256 rate = model.getBorrowRatePerYear(80e16 + 1);
assertGt(rate, 6e16); // 应该略高于 6%
}
// Fuzz 测试
function testFuzz_BorrowRate_AlwaysPositive(uint256 utilization) public {
utilization = bound(utilization, 0, 100e16);
uint256 rate = model.getBorrowRatePerYear(utilization);
assertGe(rate, 2e16); // 至少是 baseRate
}
function testFuzz_BorrowRate_Monotonic(uint256 u1, uint256 u2) public {
u1 = bound(u1, 0, 100e16);
u2 = bound(u2, u1, 100e16);
// 利率应该随利用率单调递增
assertGe(
model.getBorrowRatePerYear(u2),
model.getBorrowRatePerYear(u1)
);
}
}
// ============ Supply/Withdraw 测试 ============
contract SupplyWithdrawTest is Test {
// 基础功能
function test_Supply_FirstDepositor() public {}
function test_Supply_SecondDepositor() public {}
function test_Supply_AfterInterestAccrual() public {}
function test_Withdraw_Full() public {}
function test_Withdraw_Partial() public {}
function test_Withdraw_MaxUint256() public {}
// 边界条件
function test_Supply_MinimumAmount() public {}
function test_Supply_ZeroAmount_Reverts() public {}
function test_Withdraw_MoreThanBalance_Reverts() public {}
function test_Withdraw_InsufficientLiquidity_Reverts() public {}
// 通胀攻击防护
function test_InflationAttack_Mitigated() public {
// 步骤 1:攻击者存入 1 wei
vm.prank(attacker);
pool.supply(address(usdc), 1);
// 步骤 2:攻击者直接转入大量代币
vm.prank(attacker);
usdc.transfer(address(pool), 10_000e6);
// 步骤 3:受害者存入
vm.prank(victim);
uint256 shares = pool.supply(address(usdc), 10_000e6);
// 验证:受害者获得了合理数量的份额
assertGt(shares, 0, "Victim should get shares");
// 验证:受害者取出时不会损失过多
vm.prank(victim);
uint256 withdrawAmount = pool.withdraw(address(usdc), shares);
assertGt(withdrawAmount, 9_999e6, "Victim should not lose significant value");
}
// Fuzz 测试
function testFuzz_SupplyWithdraw_NoProfit(uint256 amount) public {
amount = bound(amount, 1, 1e30);
// 存入再立即取出,不应该获利
vm.startPrank(alice);
usdc.mint(alice, amount);
usdc.approve(address(pool), amount);
uint256 shares = pool.supply(address(usdc), amount);
uint256 withdrawn = pool.withdraw(address(usdc), shares);
vm.stopPrank();
assertLe(withdrawn, amount, "Should not profit from immediate withdraw");
}
}
// ============ Borrow/Repay 测试 ============
contract BorrowRepayTest is Test {
function test_Borrow_BasicFlow() public {}
function test_Borrow_ExceedsLTV_Reverts() public {}
function test_Borrow_MultipleAssets() public {}
function test_Repay_Full() public {}
function test_Repay_Partial() public {}
function test_Repay_WithAccruedInterest() public {}
// 健康因子测试
function test_HealthFactor_NoDebt_MaxUint() public {}
function test_HealthFactor_SafePosition() public {}
function test_HealthFactor_AtRisk() public {}
function test_HealthFactor_Underwater() public {}
function test_HealthFactor_PriceChange() public {}
// 利息累计测试
function test_Interest_IncreasesDebt() public {
// 设置
setupBorrowPosition(); // Alice 存 ETH,借 USDC
uint256 debtBefore = pool.getUserDebt(alice, address(usdc));
// 推进时间
vm.warp(block.timestamp + 365 days);
pool.accrueInterest(address(usdc)); // 触发更新
uint256 debtAfter = pool.getUserDebt(alice, address(usdc));
assertGt(debtAfter, debtBefore, "Debt should increase");
// 验证利息金额合理
uint256 interest = debtAfter - debtBefore;
uint256 expectedInterest = debtBefore * 5 / 100; // 大约 5% 年化
assertApproxEqRel(interest, expectedInterest, 5e16); // 5% 误差容忍
}
}
// ============ 清算测试(Day 80 实现) ============
contract LiquidationTest is Test {
function test_Liquidate_BasicFlow() public {}
function test_Liquidate_HealthFactorOk_Reverts() public {}
function test_Liquidate_PartialLiquidation() public {}
function test_Liquidate_BonusCalculation() public {}
function test_Liquidate_BadDebt() public {}
}
4. 安全审查检查清单
基于 Week 13 学到的所有安全知识,对 Mini Lending 进行审查:
## Mini Lending 安全审查清单
### 重入防护
- [x] 使用 ReentrancyGuard (nonReentrant)
- [x] 状态先更新,再进行外部调用 (CEI 模式)
- [ ] 检查所有外部调用点
### 整数溢出
- [x] Solidity 0.8+ 自动检查
- [ ] 检查 unchecked 块使用是否安全
- [ ] 中间计算是否可能超过 uint256
### 预言机安全
- [x] Chainlink 价格正值检查
- [x] 新鲜度检查 (MAX_PRICE_AGE)
- [x] 轮次完整性检查
- [ ] 价格精度标准化是否正确
- [ ] L2 Sequencer 检查(如果部署在 L2)
- [ ] 极端价格的处理(价格为 0、极大值)
### 份额计算
- [x] 虚拟偏移防通胀攻击
- [x] 存款向下取整(保护协议)
- [x] 取款向下取整(保护协议)
- [ ] 检查除零情况
- [ ] 精度损失是否在可接受范围内
### 借贷逻辑
- [x] 借款前检查健康因子
- [x] 取款前应检查健康因子(有借款时)
- [ ] 利息累计的精度验证
- [ ] borrowIndex 增长是否可能溢出
- [ ] 全额还款时的精度处理
### 清算机制(Day 80 实现时检查)
- [ ] 只在 HF < 1 时允许清算
- [ ] 清算后借款人 HF 应该改善
- [ ] 清算奖励不应超过抵押品价值
- [ ] 坏账情况的处理
### 权限控制
- [x] 管理函数有 onlyOwner
- [ ] 暂停机制(紧急情况)
- [ ] 市场配置参数的合理范围检查
### 通用
- [ ] 所有 external 函数的入参验证
- [ ] 事件是否正确记录所有关键操作
- [ ] 升级机制(如果需要)
5. Solana 技能回顾
Solana 开发技能树 (截至 Day 78)
├── 基础
│ ├── 账户模型理解 ✅
│ ├── PDA 推导和使用 ✅
│ ├── CPI (跨程序调用) ✅
│ └── SPL Token 操作 ✅
│
├── Anchor 框架
│ ├── 账户约束 (seeds, has_one, constraint) ✅
│ ├── 自定义错误 (#[error_code]) ✅
│ ├── 事件 (emit!) ✅
│ ├── Zero-Copy (#[zero_copy]) ✅
│ ├── remaining_accounts ✅
│ └── init_if_needed ✅
│
├── 安全
│ ├── Signer 验证 ✅
│ ├── Owner 验证 ✅
│ ├── PDA 种子碰撞 ✅
│ ├── 整数溢出 (checked_*) ✅
│ ├── 账户类型混淆 ✅
│ └── 安全关闭账户 ✅
│
├── 性能
│ ├── Zero-Copy 优化 ✅
│ ├── CU 预算管理 ✅
│ ├── 并行设计 ✅
│ └── Address Lookup Tables ✅
│
└── 项目实战
├── 基础 Vault ✅
├── 增强 Escrow ✅
└── Token Swap (未实现)
6. 跨链安全对比总结
| 漏洞类型 | Solidity | Solana/Anchor | Move |
|---|---|---|---|
| 重入 | 高危(需 ReentrancyGuard) | 低(CPI 不重入) | 无(语言层面消除) |
| 整数溢出 | 0.8+ 自动检查 | Release 不检查! | VM 层面检查 |
| 权限控制 | modifier(运行时) | Signer/has_one(编译+运行) | Capability(编译时) |
| 预言机操纵 | 常见 | 常见 | 常见 |
| 逻辑错误 | 常见 | 常见 | 常见 |
| 闪电贷攻击 | 常见 | Solana 原生不支持闪电贷 | Hot Potato 约束 |
| 未初始化存储 | 偶发 | 账户未验证 | 无 |
| 合约升级风险 | Proxy 模式 | 程序升级 | Package 升级 |
代码实战
Mini Lending 集成测试模板
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/MiniLendingPool.sol";
import "../src/InterestRateModel.sol";
/// @title Mini Lending 集成测试
/// @notice 测试多步骤操作流程
contract IntegrationTest is Test {
MiniLendingPool pool;
MockERC20 weth;
MockERC20 usdc;
MockChainlink ethPriceFeed;
MockChainlink usdcPriceFeed;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address liquidator = makeAddr("liquidator");
function setUp() public {
// 部署合约
pool = new MiniLendingPool();
weth = new MockERC20("Wrapped ETH", "WETH", 18);
usdc = new MockERC20("USD Coin", "USDC", 6);
ethPriceFeed = new MockChainlink(8);
ethPriceFeed.setPrice(2000e8); // $2000
usdcPriceFeed = new MockChainlink(8);
usdcPriceFeed.setPrice(1e8); // $1
// 配置市场...
// 分配代币...
}
/// @notice 完整借贷-清算流程测试
function test_FullLendingCycle() public {
// === 阶段 1:存款 ===
// Bob 存入 1,000,000 USDC 作为流动性
vm.prank(bob);
pool.supply(address(usdc), 1_000_000e6);
// Alice 存入 10 ETH 作为抵押品
vm.prank(alice);
pool.supply(address(weth), 10 ether);
// === 阶段 2:借款 ===
// Alice 借出 12,000 USDC
// LTV 80%: 10 * 2000 * 0.8 = 16,000 可借
vm.prank(alice);
pool.borrow(address(usdc), 12_000e6);
uint256 hf = pool.getHealthFactor(alice);
assertGt(hf, 1e18, "Should be healthy after borrow");
// === 阶段 3:利息累计 ===
vm.warp(block.timestamp + 30 days);
uint256 debt = pool.getUserDebt(alice, address(usdc));
assertGt(debt, 12_000e6, "Debt should accrue interest");
// === 阶段 4:价格下跌 → 清算 ===
ethPriceFeed.setPrice(1100e8); // ETH 跌到 $1100
hf = pool.getHealthFactor(alice);
assertLt(hf, 1e18, "Should be underwater");
// Liquidator 清算 Alice 的仓位
vm.prank(liquidator);
pool.liquidate(alice, address(usdc), address(weth), 6000e6);
// 验证清算后状态
hf = pool.getHealthFactor(alice);
assertGt(hf, 1e18, "Should be healthy after liquidation");
// === 阶段 5:Alice 还款 ===
uint256 remainingDebt = pool.getUserDebt(alice, address(usdc));
vm.prank(alice);
pool.repay(address(usdc), type(uint256).max);
assertEq(pool.getUserDebt(alice, address(usdc)), 0);
// === 阶段 6:Bob 取回存款 + 利息 ===
vm.prank(bob);
uint256 withdrawn = pool.withdraw(address(usdc), type(uint256).max);
assertGt(withdrawn, 1_000_000e6, "Bob should earn interest");
}
/// @notice 多用户并发场景
function test_MultiUserScenario() public {
address[] memory users = new address[](5);
for (uint i = 0; i < 5; i++) {
users[i] = makeAddr(string(abi.encodePacked("user", i)));
weth.mint(users[i], 100 ether);
usdc.mint(users[i], 1_000_000e6);
vm.startPrank(users[i]);
weth.approve(address(pool), type(uint256).max);
usdc.approve(address(pool), type(uint256).max);
// 每个用户存入不同金额
pool.supply(address(weth), (i + 1) * 10 ether);
pool.supply(address(usdc), (i + 1) * 100_000e6);
vm.stopPrank();
}
// 部分用户借款
for (uint i = 0; i < 3; i++) {
vm.prank(users[i]);
pool.borrow(address(usdc), (i + 1) * 50_000e6);
}
// 时间推进
vm.warp(block.timestamp + 90 days);
// 验证所有用户的健康因子
for (uint i = 0; i < 5; i++) {
uint256 hf = pool.getHealthFactor(users[i]);
if (pool.getUserDebt(users[i], address(usdc)) > 0) {
assertGt(hf, 0, "HF should be positive");
} else {
assertEq(hf, type(uint256).max, "No debt = max HF");
}
}
}
}
/// @title Mock Chainlink 预言机
contract MockChainlink {
int256 public price;
uint8 public decimals_;
uint256 public updatedAt;
constructor(uint8 _decimals) {
decimals_ = _decimals;
updatedAt = block.timestamp;
}
function setPrice(int256 _price) external {
price = _price;
updatedAt = block.timestamp;
}
function decimals() external view returns (uint8) {
return decimals_;
}
function latestRoundData() external view returns (
uint80, int256, uint256, uint256, uint80
) {
return (1, price, 0, updatedAt, 1);
}
}
关键要点总结
| 维度 | 本周收获 |
|---|---|
| 安全审计 | Move 安全模型 + Solidity 闪电贷攻击 + Solana 6 大漏洞 |
| 协议开发 | Mini Lending 存款/借款/利息/预言机 全部实现 |
| Solana 进阶 | Zero-Copy/CU 优化/并行模型/增强 Escrow |
| 测试策略 | 单元/集成/Fuzz/边界 全面覆盖 |
| 跨链视野 | EVM vs Solana vs Move 安全对比 |
常见误区
- "测试通过 = 代码安全" — 测试覆盖的只是已知场景,未知的攻击向量需要专业审计
- "复习等于浪费时间" — 错误!系统性回顾能发现知识盲区和关联关系
- "Mini Lending 只是练习没有实际价值" — 这是理解 DeFi 协议架构的最佳方式,也是面试中的强力展示
- "Solidity 和 Solana 选一个就够了" — 多链能力在求职市场极具竞争力
- "安全检查做一次就够了" — 每次代码变更都应该重新检查
面试关联
面试题:你在实现借贷协议中遇到了哪些挑战?
30 秒回答: 三个核心挑战:一是份额制会计模型中的精度和通胀攻击防护;二是利息累计的正确性(borrowIndex 机制);三是Chainlink 预言机集成中的异常处理(价格过期、精度转换、L2 Sequencer 等)。
2 分钟回答: 实现 Mini Lending 协议过程中有几个关键挑战。首先是份额制会计——使用类似 ERC4626 的模型,需要处理首个存款人通胀攻击(通过虚拟偏移解决),还需要确保每次份额和资产之间的转换方向正确(存款向下取整保护协议,取款也向下取整避免多取)。第二是利息模型的实现——分段线性利率曲线在拐点处需要正确处理,borrowIndex 的增长必须是累积性的而非覆盖性的。第三是预言机安全——不仅要获取价格,还要做新鲜度检查、轮次完整性验证、精度标准化。第四是清算机制设计——需要在保护协议(坏账防护)和保护借款人(不过度清算)之间取得平衡。整个过程最大的收获是理解了 DeFi 协议各组件之间的紧密耦合关系。
参考资源
| 资源 | 说明 |
|---|---|
| Foundry Book | Foundry 测试框架文档 |
| Aave V3 源码 | 生产级借贷协议参考 |
| Morpho Blue | 极简借贷协议实现 |
| Solana Test Validator | 本地测试验证器 |
| Trail of Bits 测试指南 | 高级 Fuzz 测试 |