首页
学习
活动
专区
圈层
工具
发布
49 篇文章
1
《纸上谈兵·solidity》第 0 课:搭建 Solidity 开发环境(三种方式)
2
《纸上谈兵·solidity》第 1 课:部署你的第一个 Solidity 合约
3
《纸上谈兵·solidity》第 2 课:调用、修改、读取,Solidity 合约不是 REST API
4
《纸上谈兵·solidity》第 3 课:事件(Event)机制与链上日志——不是 print,是广播!
5
《纸上谈兵·solidity》第 4 课:Solidity 合约中的错误处理机制(`require`、`revert`、`assert`)和自定义错误
6
《纸上谈兵·solidity》第 5 课:依赖与外部调用 —— 合约交互的风险与防护
7
《纸上谈兵·solidity》第 6 课:Solidity 数据存储布局 —— memory、storage、calldata 傻傻分不清?
8
《纸上谈兵·solidity》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签
9
《纸上谈兵·solidity》第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
10
《纸上谈兵·solidity》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具
11
《纸上谈兵·solidity》第 10 课:Solidity `fallback` / `receive` 函数 —— 合约如何收 ETH 和响应未知调用?
12
《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败
13
《纸上谈兵·solidity》第 12 课:Solidity 函数选择器与 ABI 编码原理
14
《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”
15
《纸上谈兵·solidity》第 14 课:Solidity 中的可升级合约模式 —— 从代理合约到透明代理、UUPS 与安全陷阱
16
《纸上谈兵·solidity》第 15 课:Solidity 库与可重用代码
17
《纸上谈兵·solidity》第 16 课:Pull over Push 支付模式与 Check-Effects-Interactions 原则
18
《纸上谈兵·solidity》第 17 课:合约设计模式实战(二)—— Access Control 与权限管理
19
《纸上谈兵·solidity》第 18 课:合约设计模式实战(三)—— 代理 + 插件化架构(Diamond Standard / EIP-2535)
20
《纸上谈兵·solidity》第 19 课:安全专题(一)—— 常见攻击手法与防御
21
《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞
22
《纸上谈兵·solidity》第 21 课:Gas 优化与成本分析 —— 写出便宜的智能合约
23
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
24
《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战
25
《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战
26
《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)简化版
27
《纸上谈兵·solidity》第 26 课:借贷合约简化实现
28
《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)
29
《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)
30
《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)
31
《纸上谈兵·solidity》第 30 课:智能合约安全审计案例复盘 -- Nomad Bridge(2022)
32
《纸上谈兵·solidity》第 31 课:多签钱包在跨链桥中的应用 —— Nomad 事件复盘
33
《纸上谈兵·solidity》第 32 课:DeFi 基础合约
34
《纸上谈兵·solidity》第 33 课:多签钱包(Multisig Wallet)-- 合约设计与实现
35
《纸上谈兵·solidity》第 34 课:多签钱包(Multisig Wallet)-- 上线
36
《纸上谈兵·solidity》第 35 课:去中心化交易所(DEX)实战 — 合约设计
37
《纸上谈兵·solidity》第 36 课:去中心化交易所(DEX)实战 — 上线
38
《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型
39
《纸上谈兵·solidity》第 38 课:DeFi 实战(2) -- 清算机制与价格预言机
40
《纸上谈兵·solidity》第 39 课:DeFi 实战(3) -- 利息累积与 aToken 设计
41
《纸上谈兵·solidity》第 40 课:DeFi 实战(4) -- 风险控制与防护
42
《纸上谈兵·solidity》第 41 课:DeFi 实战(5) -- 协议费与治理
43
《纸上谈兵·solidity》第 42 课:DeFi 实战(6) -- 跨资产借贷与多市场支持
44
《纸上谈兵·solidity》第 43 课:DeFi 实战(7) -- 清算机制进阶(多资产抵押清算路径、拍卖机制)
45
《纸上谈兵·solidity》第 44 课:DeFi 实战(8) -- 利率曲线与资金池优化(动态利用率模型)
46
《纸上谈兵·solidity》第 45 课:DeFi 实战(9) -- 利息累积与结算机制(可复利)
47
《纸上谈兵·solidity》第 46 课:DeFi 实战(10) -- 跨链借贷与流动性桥接
48
《纸上谈兵·solidity》第 47 课:DeFi 实战(11) -- 治理代币 & 激励机制(Tokenomics & Governance)
49
《纸上谈兵·solidity》第 48 课:DeFi 实战(12) -- 前端 DApp 集成与用户交互(React + ethers.js 实战)

《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型

--

1、学习目标

  • 理解借贷平台的 资金池机制
  • 掌握 Aave / Compound 等平台的 动态利率模型
  • 编写一个简化的 带利率的借贷池合约
  • 使用 Foundry 编写测试,验证利率随资金利用率变化

2、知识点梳理

  1. 资金池(Lending Pool)
    • 所有存款用户的资产进入一个共享池子
    • 借款人从池子中提取资金
    • 池子内资金利用率决定利率水平
  2. 资金利用率(Utilization Rate, U) U = \frac{总借款}{总存款}
    • U 越高,说明池子资金越紧张,借款利率越高
    • U 越低,说明资金富余,借款利率越低
  3. 利率模型(Interest Rate Model)
    • 基础利率(Base Rate):当利用率接近 0 时的最低借款利率
    • 斜率(Slope):利用率上升时,利率增加的速度
    • 最优利用率(Optimal Utilization):一个转折点,超过该点后利率会陡增,防止资金池被借空
  4. 存款利率(Supply Rate) 存款利率来自借款利息分配: 存款利率 = 借款利率 \times \frac{总借款}{总存款} \times (1 - 协议费率)

3、资金池合约实现

LendingPoolWithRate.sol

代码语言:solidity
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// 导入 OpenZeppelin 的 ERC20 接口和安全转账工具
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
 * @title LendingPoolWithRate
 * @dev 借贷池合约,支持动态利率计算。
 * 该合约允许用户存入和借出 ERC20 代币,并根据资金利用率动态调整借款利率。
 */
contract LendingPoolWithRate {
    using SafeERC20 for IERC20;

    // 借贷池支持的资产(ERC20 代币)
    IERC20 public asset;

    // 总存款量
    uint256 public totalDeposits;
    // 总借款量
    uint256 public totalBorrows;

    // 用户存款映射(地址 => 存款量)
    mapping(address => uint256) public deposits;
    // 用户借款映射(地址 => 借款量)
    mapping(address => uint256) public borrows;

    // 利率模型参数
    uint256 public baseRate = 2e16;       // 基础利率(2%)
    uint256 public slope = 10e16;         // 斜率(10%)
    uint256 public optimalUtilization = 80e16; // 最优资金利用率(80%)
    uint256 public constant ONE = 1e18;   // 1e18 表示 100%(用于计算)

    /**
     * @dev 构造函数,初始化借贷池的资产。
     * @param _asset 借贷池支持的 ERC20 代币地址。
     */
    constructor(IERC20 _asset) {
        asset = _asset;
    }

    /**
     * @dev 存款函数:用户将资产存入借贷池。
     * @param amount 存款数量。
     */
    function deposit(uint256 amount) external {
        asset.safeTransferFrom(msg.sender, address(this), amount);
        deposits[msg.sender] += amount;
        totalDeposits += amount;
    }

    /**
     * @dev 借款函数:用户从借贷池借出资产。
     * @param amount 借款数量。
     */
    function borrow(uint256 amount) external {
        require(totalDeposits - totalBorrows >= amount, "insufficient liquidity");
        borrows[msg.sender] += amount;
        totalBorrows += amount;
        asset.safeTransfer(msg.sender, amount);
    }

    /**
     * @dev 计算当前资金利用率(借款量 / 存款量)。
     * @return 资金利用率(百分比,以 1e18 表示 100%)。
     */
    function getUtilization() public view returns (uint256) {
        if (totalDeposits == 0) return 0;
        return (totalBorrows * ONE) / totalDeposits;
    }

    /**
     * @dev 计算动态借款利率。
     * @return 借款利率(百分比,以 1e18 表示 100%)。
     */
    function getBorrowRate() public view returns (uint256) {
        uint256 utilization = getUtilization();
        if (utilization <= optimalUtilization) {
            return baseRate + (utilization * slope) / optimalUtilization;
        } else {
            uint256 excess = utilization - optimalUtilization;
            return baseRate + slope + (excess * slope) / (ONE - optimalUtilization);
        }
    }

    /**
     * @dev 计算存款利率(协议抽取 10% 利息)。
     * @return 存款利率(百分比,以 1e18 表示 100%)。
     */
    function getSupplyRate() public view returns (uint256) {
        if (totalDeposits == 0) return 0;
        uint256 borrowRate = getBorrowRate();
        return (borrowRate * totalBorrows * 90) / (totalDeposits * 100);
    }
}

4、Foundry 测试

LendingPoolWithRateTest.sol

代码语言:solidity
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/LendingPoolWithRate.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @title MockERC20
 * @dev 模拟 ERC20 代币合约,用于测试。
 */
contract MockERC20 is ERC20 {
    constructor(string memory n, string memory s) ERC20(n, s) {}
    /**
     * @dev 铸造代币。
     * @param to 接收代币的地址。
     * @param amount 铸造的代币数量。
     */
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

/**
 * @title LendingPoolWithRateTest
 * @dev 测试借贷池合约的功能,包括存款、借款和利率计算。
 */
contract LendingPoolWithRateTest is Test {
    LendingPoolWithRate pool;
    MockERC20 usdc;

    address alice = address(0x1);
    address bob = address(0x2);
    address charlie = address(0x3);

    /**
     * @dev 初始化测试环境,部署 MockERC20 和 LendingPoolWithRate 合约。
     */
    function setUp() public {
        usdc = new MockERC20("USD Coin", "USDC");
        pool = new LendingPoolWithRate(IERC20(address(usdc)));

        usdc.mint(alice, 1000 ether);
        usdc.mint(bob, 1000 ether);
        usdc.mint(charlie, 1000 ether);
    }

    /**
     * @dev 测试存款量为零时的行为。
     */
    function testDepositZeroAmount() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 0);
        pool.deposit(0);
        vm.stopPrank();

        assertEq(pool.deposits(alice), 0);
        assertEq(pool.totalDeposits(), 0);
    }

    /**
     * @dev 测试借款量为零时的行为。
     */
    function testBorrowZeroAmount() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        pool.borrow(0);
        vm.stopPrank();

        assertEq(pool.borrows(bob), 0);
        assertEq(pool.totalBorrows(), 0);
    }

    /**
     * @dev 测试存款和借款利率的变化逻辑。
     */
    function testDepositAndBorrowRateChange() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        // 初始 U=0,利率接近 baseRate
        assertApproxEqAbs(pool.getBorrowRate(), 0.02 ether, 0.001 ether);

        // Bob 借走 500,U=50%
        vm.startPrank(bob);
        pool.borrow(500 ether);
        vm.stopPrank();
        assertGt(pool.getBorrowRate(), 0.02 ether);

        // Bob 再借 300,U=80%,接近 optimal
        vm.startPrank(bob);
        pool.borrow(300 ether);
        vm.stopPrank();
        uint256 rateAt80 = pool.getBorrowRate();

        // 再借 100,U=90%,利率应大幅上升
        vm.startPrank(bob);
        pool.borrow(100 ether);
        vm.stopPrank();
        uint256 rateAt90 = pool.getBorrowRate();

        assertGt(rateAt90, rateAt80);
    }

    /**
     * @dev 测试多个用户同时操作的情况
     */
    function testMultipleUsers() public {
        // 三个用户分别存款
        vm.startPrank(alice);
        usdc.approve(address(pool), 500 ether);
        pool.deposit(500 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        usdc.approve(address(pool), 300 ether);
        pool.deposit(300 ether);
        vm.stopPrank();

        vm.startPrank(charlie);
        usdc.approve(address(pool), 200 ether);
        pool.deposit(200 ether);
        vm.stopPrank();

        assertEq(pool.totalDeposits(), 1000 ether);

        // 多个用户借款
        vm.startPrank(alice);
        pool.borrow(200 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        pool.borrow(300 ether);
        vm.stopPrank();

        vm.startPrank(charlie);
        pool.borrow(400 ether);
        vm.stopPrank();

        assertEq(pool.totalBorrows(), 900 ether);
    }

    /**
     * @dev 测试存款利率的计算逻辑。
     */
    function testSupplyRate() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        pool.borrow(500 ether); // U=50%
        vm.stopPrank();

        uint256 supplyRate = pool.getSupplyRate();
        assertGt(supplyRate, 0); // 存款人应该获得收益
    }

    /**
     * @dev 测试资金利用率为100%时的利率。
     */
    function testFullUtilizationRate() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        pool.borrow(1000 ether); // U=100%
        vm.stopPrank();

        uint256 borrowRate = pool.getBorrowRate();
        assertGt(borrowRate, 0.02 ether); // 利率应显著高于基础利率
    }

    /**
     * @dev 测试资金利用率为0%的情况
     */
    function testZeroUtilization() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        // 不进行任何借款,资金利用率为0%
        assertEq(pool.getUtilization(), 0);
        
        // 借款利率应该等于基础利率
        assertEq(pool.getBorrowRate(), pool.baseRate());
        
        // 存款利率应该为0
        assertEq(pool.getSupplyRate(), 0);
    }

    /**
     * @dev 测试利率计算的数学边界
     */
    function testRateCalculationEdgeCases() public {
        // 测试空池情况
        assertEq(pool.getUtilization(), 0);
        assertEq(pool.getBorrowRate(), pool.baseRate());
        assertEq(pool.getSupplyRate(), 0);

        // 存入极小金额测试除法边界
        vm.startPrank(alice);
        usdc.approve(address(pool), 1);
        pool.deposit(1);
        vm.stopPrank();

        // 即使只有1 wei,利率计算也不应该revert
        pool.getBorrowRate();
        pool.getSupplyRate();
    }

    /**
     * @dev 测试在空池状态下借款
     */
    function test_RevertWhen_BorrowFromEmptyPool() public {
        vm.startPrank(alice);
        // 不存入任何资金,直接尝试借款
        vm.expectRevert("insufficient liquidity");
        pool.borrow(100 ether);
        vm.stopPrank();
    }

    /**
     * @dev 测试借款超过存款量时的错误处理。
     */
    function test_RevertWhen_BorrowExceedsDeposit() public {
        vm.startPrank(alice);
        usdc.approve(address(pool), 1000 ether);
        pool.deposit(1000 ether);
        vm.stopPrank();

        vm.startPrank(bob);
        // 尝试借出超过可用流动性的金额
        vm.expectRevert("insufficient liquidity");
        pool.borrow(1001 ether);
        vm.stopPrank();
    }

    /**
     * @dev 测试无批准情况下的存款
     */
    function test_RevertWhen_DepositWithoutApproval() public {
        vm.startPrank(alice);
        // 不进行approve,直接存款
        vm.expectRevert(); // SafeERC20 会revert
        pool.deposit(100 ether);
        vm.stopPrank();
    }
}

执行测试:

代码语言:bash
复制
➜  defi git:(main) ✗ forge test --match-path test/LendingPoolWithRate.t.sol -vvv
[⠊] Compiling...
[⠔] Compiling 1 files with Solc 0.8.30
[⠒] Solc 0.8.30 finished in 483.09ms
Compiler run successful!

Ran 11 tests for test/LendingPoolWithRate.t.sol:LendingPoolWithRateTest
[PASS] testBorrowZeroAmount() (gas: 116664)
[PASS] testDepositAndBorrowRateChange() (gas: 191775)
[PASS] testDepositZeroAmount() (gas: 42655)
[PASS] testFullUtilizationRate() (gas: 148140)
[PASS] testMultipleUsers() (gas: 303127)
[PASS] testRateCalculationEdgeCases() (gas: 123003)
[PASS] testSupplyRate() (gas: 166296)
[PASS] testZeroUtilization() (gas: 115394)
[PASS] test_RevertWhen_BorrowExceedsDeposit() (gas: 104728)
[PASS] test_RevertWhen_BorrowFromEmptyPool() (gas: 16808)
[PASS] test_RevertWhen_DepositWithoutApproval() (gas: 21030)
Suite result: ok. 11 passed; 0 failed; 0 skipped; finished in 5.46ms (15.34ms CPU time)

Ran 1 test suite in 148.13ms (5.46ms CPU time): 11 tests passed, 0 failed, 0 skipped (11 total tests)

5、本课总结

  • 借贷平台核心是 资金池 → 存款 & 借款共享同一池子
  • 资金利用率 U 决定资金紧张程度
  • 利率曲线 = 基础利率 + 利用率 × 斜率
  • 存款利率来源于借款利息,协议可抽取部分作为费用
  • 我们实现了一个简化的动态利率模型,和现实 DeFi 平台思路一致

6、作业

  1. 修改合约,支持 多种资产 的存款 / 借款池(类似 Aave 的 Pool)。
  2. 在合约中加入 协议费累积逻辑(记录协议收入)。
  3. 思考:如果要支持「浮动利率借贷凭证」(aToken/cToken),应该在存款时如何设计?
下一篇
举报
领券