- 1. 学习目标
- 2. 核心知识点
- 3. 合约实现:
LendingPoolWithProtection.sol
- 4. 测试代码:
LendingPoolWithProtection.t.sol
- 5. 总结
- 6. 课后作业
1. 学习目标
- 理解借贷协议面临的核心安全风险
- 掌握如何在 Solidity 中防御常见攻击(如重入攻击、预言机操纵)
- 在资金安全与去中心化之间找到平衡
2. 核心知识点
- 重入攻击(Reentrancy Attack)
- 攻击者通过合约回调,反复调用
withdraw()
等函数,导致重复转账。 - 防御方法:
- 使用
ReentrancyGuard
(OpenZeppelin 提供) - 遵循 Checks-Effects-Interactions 模式
- 使用
- 攻击者通过合约回调,反复调用
- 预言机操纵(Oracle Manipulation)
- 攻击者通过闪电贷操纵交易对价格,导致借贷协议错误清算或套利。
- 防御方法:
- 使用去中心化预言机(如 Chainlink)
- 设置价格更新延迟,避免瞬时波动影响
- 采用多源价格聚合
- 利率与资金池风险
- 资金池枯竭(借款率 100%)时,存款人无法提现。
- 防御方法:
- 设置借款上限(Reserve Factor)
- 协议保留部分流动性
3. 合约实现:LendingPoolWithProtection.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title 价格预言机接口
* @notice 提供获取代币价格的功能
*/
interface IPriceOracle {
/**
* @notice 获取指定代币的当前价格
* @param token 要查询价格的代币地址
* @return 代币价格,以基础计价单位表示
*/
function getPrice(address token) external view returns (uint256);
}
/**
* @title 带保护机制的借贷池合约
* @notice 允许用户存款、取款、借款和还款,包含重入保护和借款上限机制
* @dev 使用ReentrancyGuard防止重入攻击,通过价格预言机获取资产价格
*/
contract LendingPoolWithProtection is ReentrancyGuard {
using SafeERC20 for ERC20;
/// @notice 存款事件,当用户存入资产时触发
event Deposit(address indexed user, uint256 amount);
/// @notice 取款事件,当用户取出资产时触发
event Withdraw(address indexed user, uint256 amount);
/// @notice 借款事件,当用户借出资产时触发
event Borrow(address indexed user, uint256 amount);
/// @notice 还款事件,当用户偿还借款时触发
event Repay(address indexed user, uint256 amount);
/// @notice 借贷池支持的ERC20资产
ERC20 public immutable asset;
/// @notice 价格预言机合约,用于获取资产价格
IPriceOracle public immutable oracle;
/// @notice 用户地址到存款金额的映射
mapping(address => uint256) public deposits;
/// @notice 用户地址到借款金额的映射
mapping(address => uint256) public borrows;
/// @notice 合约中总存款金额
uint256 public totalDeposits;
/// @notice 合约中总借款金额
uint256 public totalBorrows;
/// @notice 借款上限比例,基于总存款的百分比
uint256 public constant BORROW_CAP = 80; // 最大 80% 资金可借出
/**
* @notice 构造函数,初始化借贷池
* @param _asset 借贷池支持的ERC20代币地址
* @param _oracle 价格预言机合约地址
*/
constructor(address _asset, address _oracle) {
asset = ERC20(_asset);
oracle = IPriceOracle(_oracle);
}
/**
* @notice 存款功能,用户将资产存入借贷池
* @dev 使用nonReentrant修饰符防止重入攻击
* @param amount 存款金额
*/
function deposit(uint256 amount) external nonReentrant {
require(amount > 0, "invalid amount");
asset.safeTransferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
totalDeposits += amount;
emit Deposit(msg.sender, amount);
}
/**
* @notice 借款功能,用户从借贷池借出资产
* @dev 借款金额不能超过借款上限,使用nonReentrant修饰符防止重入攻击
* @param amount 借款金额
*/
function borrow(uint256 amount) external nonReentrant {
require(amount > 0, "invalid amount");
uint256 cap = (totalDeposits * BORROW_CAP) / 100;
require(totalBorrows + amount <= cap, "borrow cap reached");
borrows[msg.sender] += amount;
totalBorrows += amount;
asset.safeTransfer(msg.sender, amount);
emit Borrow(msg.sender, amount);
}
/**
* @notice 还款功能,用户偿还借款
* @dev 还款金额不能超过用户的借款总额,使用nonReentrant修饰符防止重入攻击
* @param amount 还款金额
*/
function repay(uint256 amount) external nonReentrant {
require(amount > 0, "invalid amount");
require(borrows[msg.sender] >= amount, "repay too much");
asset.safeTransferFrom(msg.sender, address(this), amount);
borrows[msg.sender] -= amount;
totalBorrows -= amount;
emit Repay(msg.sender, amount);
}
/**
* @notice 取款功能,用户从借贷池取出存款
* @dev 取款金额不能超过用户存款和合约可用流动性,使用nonReentrant修饰符防止重入攻击
* @param amount 取款金额
*/
function withdraw(uint256 amount) external nonReentrant {
require(deposits[msg.sender] >= amount, "not enough deposit");
uint256 available = asset.balanceOf(address(this));
require(amount <= available, "not enough liquidity");
deposits[msg.sender] -= amount;
totalDeposits -= amount;
asset.safeTransfer(msg.sender, amount);
emit Withdraw(msg.sender, amount);
}
/**
* @notice 获取资产当前价格
* @dev 通过价格预言机查询资产价格
* @return 资产当前价格
*/
function getAssetPrice() external view returns (uint256) {
return oracle.getPrice(address(asset));
}
}
4. 测试代码:LendingPoolWithProtection.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/LendingPoolWithProtection.sol";
/**
* @title MockERC20
* @notice 用于测试的模拟ERC20代币合约
* @dev 继承OpenZeppelin的ERC20实现,提供mint功能用于测试
*/
contract MockERC20 is ERC20 {
/**
* @notice 构造函数,初始化代币
* @dev 铸造初始供应量给部署者
*/
constructor() ERC20("MockToken", "MTK") {
_mint(msg.sender, 1_000_000 ether);
}
/**
* @notice 铸造代币
* @dev 仅供测试使用,为指定地址铸造指定数量的代币
* @param to 接收代币的地址
* @param amount 铸造数量
*/
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
/**
* @title MockOracle
* @notice 用于测试的模拟价格预言机合约
* @dev 实现IPriceOracle接口,允许手动设置价格
*/
contract MockOracle is IPriceOracle {
/// @notice 当前价格
uint256 public price = 1e18;
/**
* @notice 获取代币价格
* @dev 忽略token参数,返回固定价格
* @param token 代币地址(未使用)
* @return 当前设置的价格
*/
function getPrice(address token) external view returns (uint256) {
return price;
}
/**
* @notice 设置新的价格
* @dev 仅供测试使用,更新预言机价格
* @param newPrice 新的价格值
*/
function setPrice(uint256 newPrice) external {
price = newPrice;
}
}
/**
* @title LendingPoolWithProtectionTest
* @notice 借贷池合约的完整测试套件
* @dev 使用Forge测试框架测试LendingPoolWithProtection合约的所有功能
*/
contract LendingPoolWithProtectionTest is Test {
/// @notice 测试用的ERC20代币
MockERC20 public token;
/// @notice 测试用的价格预言机
MockOracle public oracle;
/// @notice 被测试的借贷池合约
LendingPoolWithProtection public pool;
/// @notice 测试用户地址
address owner = address(this);
address user1 = address(0x123);
address user2 = address(0x234);
address user3 = address(0x345);
/// @notice 借款上限常量
uint256 constant BORROW_CAP = 80;
/// @notice 测试事件声明
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);
/**
* @notice 测试设置函数
* @dev 在每个测试运行前执行,初始化测试环境
*/
function setUp() public {
// 部署测试合约
token = new MockERC20();
oracle = new MockOracle();
pool = new LendingPoolWithProtection(address(token), address(oracle));
// 分配代币给测试用户
token.transfer(user1, 1000 ether);
token.transfer(user2, 1000 ether);
token.transfer(user3, 1000 ether);
// 授权池子操作代币
vm.startPrank(user1);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
vm.startPrank(user2);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
vm.startPrank(user3);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
}
// ============ 存款测试 ============
/**
* @notice 测试成功存款场景
* @dev 验证存款后状态正确更新,事件正确触发
*/
function test_Deposit_Success() public {
vm.startPrank(user1);
uint256 initialBalance = token.balanceOf(user1);
uint256 depositAmount = 100 ether;
// 验证事件
vm.expectEmit(true, true, true, true);
emit Deposit(user1, depositAmount);
pool.deposit(depositAmount);
// 验证状态更新
assertEq(pool.deposits(user1), depositAmount);
assertEq(pool.totalDeposits(), depositAmount);
assertEq(token.balanceOf(user1), initialBalance - depositAmount);
assertEq(token.balanceOf(address(pool)), depositAmount);
vm.stopPrank();
}
/**
* @notice 测试存款零金额时的回退
* @dev 验证合约拒绝零金额存款
*/
function test_RevertWhen_Deposit_ZeroAmount() public {
vm.startPrank(user1);
vm.expectRevert("invalid amount");
pool.deposit(0);
vm.stopPrank();
}
/**
* @notice 测试多用户存款场景
* @dev 验证多个用户存款时总存款和用户存款正确更新
*/
function test_Deposit_MultipleUsers() public {
// 用户1存款
vm.prank(user1);
pool.deposit(100 ether);
assertEq(pool.deposits(user1), 100 ether);
// 用户2存款
vm.prank(user2);
pool.deposit(200 ether);
assertEq(pool.deposits(user2), 200 ether);
// 验证总存款
assertEq(pool.totalDeposits(), 300 ether);
assertEq(token.balanceOf(address(pool)), 300 ether);
}
// ============ 取款测试 ============
/**
* @notice 测试成功取款场景
* @dev 验证取款后状态正确更新,事件正确触发
*/
function test_Withdraw_Success() public {
vm.startPrank(user1);
// 先存款
pool.deposit(100 ether);
uint256 initialPoolBalance = token.balanceOf(address(pool));
uint256 withdrawAmount = 50 ether;
// 验证事件
vm.expectEmit(true, true, true, true);
emit Withdraw(user1, withdrawAmount);
pool.withdraw(withdrawAmount);
// 验证状态更新
assertEq(pool.deposits(user1), 50 ether);
assertEq(pool.totalDeposits(), 50 ether);
assertEq(
token.balanceOf(address(pool)),
initialPoolBalance - withdrawAmount
);
vm.stopPrank();
}
/**
* @notice 测试取款超过存款金额时的回退
* @dev 验证合约拒绝超额取款
*/
function test_RevertWhen_Withdraw_InsufficientDeposit() public {
vm.startPrank(user1);
pool.deposit(100 ether);
vm.expectRevert("not enough deposit");
pool.withdraw(150 ether);
vm.stopPrank();
}
/**
* @notice 测试取款超过合约流动性时的回退
* @dev 验证当合约流动性不足时拒绝取款
*/
function test_RevertWhen_Withdraw_InsufficientLiquidity() public {
vm.startPrank(user1);
pool.deposit(100 ether);
vm.stopPrank();
// 用户2借款,消耗流动性
vm.prank(user2);
pool.borrow(80 ether);
// 用户1尝试提取超过可用流动性的金额
vm.prank(user1);
vm.expectRevert("not enough liquidity");
pool.withdraw(50 ether); // 池子只有20 ether流动性
}
/**
* @notice 测试全额取款场景
* @dev 验证用户可以取回全部存款
*/
function test_Withdraw_AllDeposit() public {
vm.startPrank(user1);
pool.deposit(100 ether);
pool.withdraw(100 ether);
assertEq(pool.deposits(user1), 0);
assertEq(pool.totalDeposits(), 0);
assertEq(token.balanceOf(user1), 1000 ether); // 余额恢复
vm.stopPrank();
}
// ============ 借款测试 ============
/**
* @notice 测试成功借款场景
* @dev 验证借款后状态正确更新,事件正确触发
*/
function test_Borrow_Success() public {
// 用户1存款提供流动性
vm.prank(user1);
pool.deposit(100 ether);
vm.startPrank(user2);
uint256 borrowAmount = 50 ether;
// 验证事件
vm.expectEmit(true, true, true, true);
emit Borrow(user2, borrowAmount);
pool.borrow(borrowAmount);
// 验证状态更新
assertEq(pool.borrows(user2), borrowAmount);
assertEq(pool.totalBorrows(), borrowAmount);
assertEq(token.balanceOf(user2), 1000 ether + borrowAmount);
vm.stopPrank();
}
/**
* @notice 测试借款零金额时的回退
* @dev 验证合约拒绝零金额借款
*/
function test_RevertWhen_Borrow_ZeroAmount() public {
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
vm.expectRevert("invalid amount");
pool.borrow(0);
}
/**
* @notice 测试超过借款上限时的回退
* @dev 验证合约拒绝超过借款上限的借款请求
*/
function test_RevertWhen_Borrow_CapLimit() public {
vm.prank(user1);
pool.deposit(100 ether);
vm.startPrank(user2);
// 借款达到上限 (80% of 100 = 80 ether)
pool.borrow(80 ether);
// 尝试再借1 wei,应该失败
vm.expectRevert("borrow cap reached");
pool.borrow(1);
vm.stopPrank();
}
/**
* @notice 测试多用户在借款上限内借款
* @dev 验证多个用户可以共享借款额度
*/
function test_Borrow_MultipleUsersUnderCap() public {
vm.prank(user1);
pool.deposit(100 ether);
// 用户2借款
vm.prank(user2);
pool.borrow(40 ether);
assertEq(pool.borrows(user2), 40 ether);
// 用户3借款
vm.prank(user3);
pool.borrow(40 ether);
assertEq(pool.borrows(user3), 40 ether);
// 验证总借款
assertEq(pool.totalBorrows(), 80 ether);
assertEq(
pool.totalBorrows(),
(pool.totalDeposits() * BORROW_CAP) / 100
);
}
/**
* @notice 测试无流动性时的借款回退
* @dev 验证当合约没有足够代币时借款失败
*/
function test_RevertWhen_Borrow_NoLiquidity() public {
// 没有存款,直接借款
vm.prank(user1);
vm.expectRevert(); // 由于余额不足,transfer会失败
pool.borrow(10 ether);
}
// ============ 还款测试 ============
/**
* @notice 测试成功还款场景
* @dev 验证还款后状态正确更新,事件正确触发
*/
function test_Repay_Success() public {
// 设置借款
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
pool.borrow(50 ether);
vm.startPrank(user2);
uint256 repayAmount = 30 ether;
// 验证事件
vm.expectEmit(true, true, true, true);
emit Repay(user2, repayAmount);
pool.repay(repayAmount);
// 验证状态更新
assertEq(pool.borrows(user2), 20 ether);
assertEq(pool.totalBorrows(), 20 ether);
assertEq(token.balanceOf(user2), 1000 ether + 50 ether - repayAmount);
vm.stopPrank();
}
/**
* @notice 测试还款零金额时的回退
* @dev 验证合约拒绝零金额还款
*/
function test_RevertWhen_Repay_ZeroAmount() public {
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
pool.borrow(50 ether);
vm.prank(user2);
vm.expectRevert("invalid amount");
pool.repay(0);
}
/**
* @notice 测试超额还款时的回退
* @dev 验证合约拒绝超过借款金额的还款
*/
function test_RevertWhen_Repay_ExcessAmount() public {
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
pool.borrow(50 ether);
vm.prank(user2);
vm.expectRevert("repay too much");
pool.repay(60 ether);
}
/**
* @notice 测试全额还款场景
* @dev 验证用户可以全额偿还借款
*/
function test_Repay_FullRepayment() public {
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
pool.borrow(50 ether);
vm.prank(user2);
pool.repay(50 ether);
assertEq(pool.borrows(user2), 0);
assertEq(pool.totalBorrows(), 0);
}
// ============ 借款上限逻辑测试 ============
/**
* @notice 测试借款上限计算
* @dev 验证不同存款金额下的借款上限计算正确
*/
function test_BorrowCap_Calculation() public {
// 测试不同存款金额下的借款上限计算
vm.prank(user1);
pool.deposit(123.456 ether);
uint256 expectedCap = (123.456 ether * BORROW_CAP) / 100;
vm.prank(user2);
pool.borrow(expectedCap);
assertEq(pool.totalBorrows(), expectedCap);
}
/**
* @notice 测试存款变化后的借款上限
* @dev 验证新增存款后借款上限正确更新
*/
function test_BorrowCap_AfterDepositChange() public {
// 初始存款和借款
vm.prank(user1);
pool.deposit(100 ether);
vm.prank(user2);
pool.borrow(80 ether); // 达到上限
// 增加存款,借款上限应该提高
vm.prank(user3);
pool.deposit(100 ether);
// 现在可以借更多
vm.prank(user2);
pool.borrow(80 ether); // 再借80,总共160
assertEq(pool.totalBorrows(), 160 ether);
assertEq(pool.totalBorrows(), (200 ether * BORROW_CAP) / 100);
}
// ============ 价格预言机测试 ============
/**
* @notice 测试获取资产价格功能
* @dev 验证价格预言机集成正常工作
*/
function test_GetAssetPrice() public {
uint256 price = pool.getAssetPrice();
assertEq(price, 1e18);
// 测试价格更新
oracle.setPrice(1.5e18);
price = pool.getAssetPrice();
assertEq(price, 1.5e18);
}
// ============ 边缘情况测试 ============
/**
* @notice 测试复杂交互场景
* @dev 模拟真实使用场景,验证合约在各种操作组合下的正确性
*/
function test_Complex_Scenario() public {
// 复杂场景:多个用户存款、借款、还款、取款
// 用户1存款
vm.prank(user1);
pool.deposit(200 ether);
// 用户2借款
vm.prank(user2);
pool.borrow(100 ether);
// 用户3存款
vm.prank(user3);
pool.deposit(100 ether);
// 用户2部分还款
vm.prank(user2);
pool.repay(50 ether);
// 用户3借款
vm.prank(user3);
pool.borrow(40 ether);
// 用户1取款
vm.prank(user1);
pool.withdraw(100 ether);
// 验证最终状态
assertEq(pool.deposits(user1), 100 ether);
assertEq(pool.deposits(user3), 100 ether);
assertEq(pool.borrows(user2), 50 ether);
assertEq(pool.borrows(user3), 40 ether);
assertEq(pool.totalDeposits(), 200 ether);
assertEq(pool.totalBorrows(), 90 ether);
// 验证借款上限
uint256 currentCap = (pool.totalDeposits() * BORROW_CAP) / 100;
assertTrue(pool.totalBorrows() <= currentCap);
}
/**
* @notice 测试最大借款上限利用率
* @dev 验证合约在达到最大借款上限时的行为
*/
function test_Maximum_BorrowCap_Utilization() public {
// 测试完全利用借款上限的情况
vm.prank(user1);
pool.deposit(1000 ether);
vm.prank(user2);
pool.borrow(800 ether);
assertEq(pool.totalBorrows(), 800 ether);
assertEq(
pool.totalBorrows(),
(pool.totalDeposits() * BORROW_CAP) / 100
);
}
}
执行测试:
➜ defi git:(master) ✗ forge test --match-path test/LendingPoolWithProtection.t.sol -vvv
[⠊] Compiling...
[⠔] Compiling 2 files with Solc 0.8.29
[⠑] Solc 0.8.29 finished in 1.48s
Compiler run successful with warnings:
Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning.
--> test/LendingPoolWithProtection.t.sol:47:23:
|
47 | function getPrice(address token) external view returns (uint256) {
| ^^^^^^^^^^^^^
Ran 21 tests for test/LendingPoolWithProtection.t.sol:LendingPoolWithProtectionTest
[PASS] test_BorrowCap_AfterDepositChange() (gas: 210930)
[PASS] test_BorrowCap_Calculation() (gas: 158634)
[PASS] test_Borrow_MultipleUsersUnderCap() (gas: 203696)
[PASS] test_Borrow_Success() (gas: 167095)
[PASS] test_Complex_Scenario() (gas: 266056)
[PASS] test_Deposit_MultipleUsers() (gas: 146024)
[PASS] test_Deposit_Success() (gas: 109991)
[PASS] test_GetAssetPrice() (gas: 20283)
[PASS] test_Maximum_BorrowCap_Utilization() (gas: 156214)
[PASS] test_Repay_FullRepayment() (gas: 140551)
[PASS] test_Repay_Success() (gas: 179424)
[PASS] test_RevertWhen_Borrow_CapLimit() (gas: 163477)
[PASS] test_RevertWhen_Borrow_NoLiquidity() (gas: 21630)
[PASS] test_RevertWhen_Borrow_ZeroAmount() (gas: 104147)
[PASS] test_RevertWhen_Deposit_ZeroAmount() (gas: 17316)
[PASS] test_RevertWhen_Repay_ExcessAmount() (gas: 163176)
[PASS] test_RevertWhen_Repay_ZeroAmount() (gas: 162940)
[PASS] test_RevertWhen_Withdraw_InsufficientDeposit() (gas: 101963)
[PASS] test_RevertWhen_Withdraw_InsufficientLiquidity() (gas: 165013)
[PASS] test_Withdraw_AllDeposit() (gas: 91461)
[PASS] test_Withdraw_Success() (gas: 118505)
Suite result: ok. 21 passed; 0 failed; 0 skipped; finished in 6.10ms (9.91ms CPU time)
Ran 1 test suite in 460.54ms (6.10ms CPU time): 21 tests passed, 0 failed, 0 skipped (21 total tests)
5. 总结
- 借贷协议面临的核心风险:
- 重入攻击:防御手段是
nonReentrant
与 CEI 模式 - 预言机操纵:防御手段是去中心化预言机 + 时间加权价格
- 流动性风险:防御手段是借款上限(Borrow Cap)
- 重入攻击:防御手段是
- 本课通过合约实现和测试,展示了如何在代码层面加固协议安全性。
6. 课后作业
- 在合约中引入 闪电贷攻击测试,模拟 Uniswap 价格操纵。
- 修改
MockOracle
,让价格在短时间内波动,测试协议能否被利用清算。 - 增加一个协议费参数,让清算时部分奖励归协议所有。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—