第二章:核心合约架构

好,咱们直接进入正题。DeFi借贷协议的核心,说白了就是一套合约的组合拳。我刚开始接触这个领域时,也以为就是简单的「存钱-借钱」逻辑。后来踩了不少坑才明白——真正的难点在于如何把利率、价格、清算、治理这几个模块捏合在一起,还要保证不出事。

这一章,我会带你拆解借贷协议的五大核心合约。每个合约我都亲手写过、改过、甚至重构过。你跟着我的思路走一遍,基本就能理解整个架构了。

2.1 借贷池(LendingPool)设计

借贷池是整个协议的心脏。用户存钱、借钱、还款、提款,全都要经过它。我见过不少新手一上来就写一个巨大的合约,把所有逻辑塞进去——结果gas费高得离谱,还容易出bug。

我个人习惯把借贷池拆成三层:

  • 入口层:处理用户交互,做参数校验
  • 逻辑层:计算利率、更新状态、触发清算
  • 数据层:存储用户余额、池子状态

来看一个简化版的借贷池核心函数:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract LendingPool {
    // 用户存款映射
    mapping(address => uint256) public deposits;
    // 用户借款映射
    mapping(address => uint256) public borrows;
    
    // 存款事件
    event Deposit(address indexed user, uint256 amount);
    event Borrow(address indexed user, uint256 amount);
    
    // 存款函数
    function deposit(uint256 amount) external {
        require(amount > 0, "Amount must be greater than 0");
        // 实际项目中还要处理aToken的铸造
        deposits[msg.sender] += amount;
        emit Deposit(msg.sender, amount);
    }
    
    // 借款函数
    function borrow(uint256 amount) external {
        require(amount > 0, "Amount must be greater than 0");
        // 这里需要检查抵押品是否充足
        require(deposits[msg.sender] >= amount, "Insufficient collateral");
        borrows[msg.sender] += amount;
        emit Borrow(msg.sender, amount);
    }
}
避坑指南: 我曾经在写借贷池时,忘了在存款函数里更新全局流动性总量。结果导致利率计算全错了,用户借不到钱。记住:每次状态变更,都要同步更新全局变量。

2.2 利率模型合约

利率模型决定了资金的使用成本。你想想看,如果利率一直不变,那资金利用率低的时候没人愿意存,高的时候又没人愿意借。所以我们需要一个动态利率模型。

目前主流的有两种:

模型类型 特点 适用场景
线性模型 利率随利用率线性增长 简单稳定币池
跳跃模型 利用率超过阈值后利率陡增 高波动资产池

我个人更推荐跳跃模型。为什么?因为当利用率超过80%时,说明资金很紧张,这时候必须用高利率来抑制借款需求,同时激励存款。我在Aave的代码里看到的就是这种设计。

// 跳跃利率模型示例
contract JumpRateModel {
    uint256 public constant BASE_RATE = 0; // 基础利率
    uint256 public constant MULTIPLIER_PER_BLOCK = 0.0005 ether; // 每块乘数
    uint256 public constant JUMP_MULTIPLIER = 0.01 ether; // 跳跃乘数
    uint256 public constant KINK = 0.8 ether; // 拐点(80%利用率)
    
    function getBorrowRate(uint256 utilization) public view returns (uint256) {
        if (utilization <= KINK) {
            return BASE_RATE + (utilization * MULTIPLIER_PER_BLOCK) / 1e18;
        } else {
            uint256 excess = utilization - KINK;
            return BASE_RATE + (KINK * MULTIPLIER_PER_BLOCK) / 1e18 + 
                   (excess * JUMP_MULTIPLIER) / 1e18;
        }
    }
}
核心要点: 利率模型参数需要根据市场情况动态调整。我建议把参数做成可治理的,这样协议可以灵活应对极端行情。

2.3 价格预言机合约

价格预言机是DeFi协议里最容易出问题的环节。我见过太多因为预言机被操纵而导致清算失败的案例。说白了,价格不准,整个协议就废了。

目前主流方案有三种:

  • Chainlink喂价:去中心化,但更新频率有限
  • Uniswap TWAP:抗操纵,但延迟较高
  • 混合方案:取多个来源的中位数

我个人强烈建议使用混合方案。为什么?因为单一来源太危险了。我曾经在项目中只用Chainlink,结果有一次节点宕机,价格卡了整整两个小时,差点引发连环清算。

// 混合预言机示例
contract PriceOracle {
    address public chainlinkFeed;
    address public uniswapPool;
    uint256 public constant TWAP_PERIOD = 30 minutes;
    
    function getPrice(address token) public view returns (uint256) {
        uint256 price1 = getChainlinkPrice(token);
        uint256 price2 = getUniswapTWAP(token, TWAP_PERIOD);
        
        // 取中位数
        if (price1 > price2) {
            return (price1 + price2) / 2;
        } else {
            return (price2 + price1) / 2;
        }
    }
}
警告: 千万不要直接用Uniswap的即时价格!那玩意儿一个闪电贷就能操纵。一定要用TWAP(时间加权平均价格),至少取30分钟以上的窗口。

2.4 清算合约

清算机制是借贷协议的安全阀。当借款人的抵押品价值低于某个阈值时,清算人就可以触发清算,拿走部分抵押品作为奖励。

清算的核心逻辑其实很简单:

  1. 检查借款人的健康因子是否小于1
  2. 计算可清算的金额(通常不超过债务的50%)
  3. 清算人偿还债务,获得抵押品(含折扣)
  4. 更新借款人的状态

嗯,这里要注意一个细节:清算奖励不能太高,否则会激励恶意清算;也不能太低,否则没人愿意当清算人。我一般建议设置在5%-10%之间。

// 清算函数核心逻辑
function liquidate(address borrower, uint256 debtToCover) external {
    // 检查健康因子
    uint256 healthFactor = getHealthFactor(borrower);
    require(healthFactor < 1e18, "Borrower is healthy");
    
    // 计算可清算金额
    uint256 maxLiquidatable = (borrowerDebt[borrower] * 50) / 100;
    require(debtToCover <= maxLiquidatable, "Exceeds liquidation limit");
    
    // 清算人偿还债务
    // 实际项目中需要处理代币转账
    borrowerDebt[borrower] -= debtToCover;
    
    // 清算人获得抵押品(含5%折扣)
    uint256 collateralToGive = (debtToCover * 105) / 100;
    // 转账抵押品给清算人
    
    emit Liquidated(borrower, msg.sender, debtToCover, collateralToGive);
}
经验之谈: 我曾经在清算合约里忘了加「只能清算一次」的限制,结果有人用闪电贷反复清算同一个借款人,把池子掏空了。记住:每个区块只能清算一次,或者加上冷却时间。

2.5 治理合约

治理合约是协议的「大脑」。它负责管理参数调整、合约升级、紧急暂停等操作。一个好的治理机制,能让协议在极端情况下快速响应。

治理合约通常包含以下功能:

  • 参数治理:调整利率模型参数、清算阈值、预言机地址等
  • 合约升级:通过代理模式升级核心合约
  • 紧急暂停:在发现漏洞时暂停所有操作
  • 多签控制:需要多个管理员共同签名才能执行操作

我个人建议采用时间锁+多签的组合方案。为什么?因为时间锁给了用户反应时间,多签防止了单点故障。我见过一个项目只用单签治理,结果私钥泄露,整个协议被清空——那叫一个惨。

// 治理合约简化版
contract Governance {
    address public timelock;
    mapping(address => bool) public admins;
    uint256 public constant MIN_DELAY = 2 days; // 最短延迟2天
    
    modifier onlyAdmin() {
        require(admins[msg.sender], "Not an admin");
        _;
    }
    
    function setInterestRate(uint256 newRate) external onlyAdmin {
        // 通过时间锁执行
        timelock.executeTransaction(
            address(interestRateModel),
            0,
            abi.encodeWithSignature("setBaseRate(uint256)", newRate),
            MIN_DELAY
        );
    }
    
    function pause() external onlyAdmin {
        // 紧急暂停不需要时间锁
        lendingPool.pause();
    }
}
核心原则: 治理权限越大,风险越高。我建议把治理操作分为「普通操作」和「危险操作」两类。普通操作(如调整利率)用时间锁,危险操作(如升级合约)需要多签+时间锁双重保护。

好了,这五个合约就是DeFi借贷协议的骨架。你可能会问:为什么没有aToken合约?为什么没有闪电贷合约?别急,那些是更细粒度的组件,我们会在后面的章节逐一展开。

记住一句话:好的架构不是功能最多,而是最容易维护和升级。我见过太多项目一开始设计得天花乱坠,结果上线后改都改不动。咱们做DeFi,稳字当头。