第二章:核心合约架构
好,咱们直接进入正题。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;
}
}
}
2.4 清算合约
清算机制是借贷协议的安全阀。当借款人的抵押品价值低于某个阈值时,清算人就可以触发清算,拿走部分抵押品作为奖励。
清算的核心逻辑其实很简单:
- 检查借款人的健康因子是否小于1
- 计算可清算的金额(通常不超过债务的50%)
- 清算人偿还债务,获得抵押品(含折扣)
- 更新借款人的状态
嗯,这里要注意一个细节:清算奖励不能太高,否则会激励恶意清算;也不能太低,否则没人愿意当清算人。我一般建议设置在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,稳字当头。