在现实的借贷协议里,利息并不是“每个用户单独按时间往账上写利息”,而是用公用索引(index)高效记录利息增长,然后按需用索引换算账户余额。这样做能极大节省 gas 并避免对每个用户频繁写状态。
interest = principal * rate * time
。 r_per_sec = annual_r / seconds_per_year
,每秒做 principal *= (1 + r_per_sec)
。 exp(r * t)
精确表示连续复利。链上实现需浮点/exp 库,复杂且 gas 贵。 scaledBorrow[user] * BI / 1e18
。当用户借入 amount
时,记录 scaledBorrowIncrease = amount * 1e18 / BI
。之后不需要每秒更新用户状态。shares
,用户的实际底层资产 = shares * SI / 1e18
。存款时 shares = amount * 1e18 / SI
。优点:只要维护全局索引(两个数)和用户的 scaled 值,就能做到对所有用户的利息进行懒惰计算(on-demand),极为高效。
reserveFactor
决定)。 interestAccrued = totalBorrows * borrowRatePerSecond * deltaTime
reservePortion = interestAccrued * reserveFactor
toDepositors = interestAccrued - reservePortion
totalBorrows += interestAccrued
;totalReserves += reservePortion
;totalDeposits += toDepositors
(或通过 supplyIndex 更新使得 aToken 持有者能看到收益)。ratePerSecond = annualRate / SECONDS_PER_YEAR
,并以整数精度(1e18)表示小数。 deltaTime
使用 block.timestamp
差值。测试中用 vm.warp()
模拟时间推进。下面实现一个教学、可运行的合约:LendingPoolInterestAccrual.sol。
要点:
borrowIndex
/ supplyIndex
(1e18 精度),按秒更新; scaledBorrow
存储;存款按 aToken
shares 记录(shares = amount * 1e18 / supplyIndex
); _accrueInterest()
,但你也可以由 keeper 定期调用以减少每次 gas(见作业)。 说明:演示合约只支持单资产市场,但索引逻辑可按市场切分扩展到多市场。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title AToken
* @notice 简单的存款凭证代币
* @dev Mint/burn 操作只能由借贷池调用
*/
contract AToken is ERC20 {
address public pool;
/**
* @notice 构造函数,初始化 aToken
* @param name_ aToken 名称
* @param symbol_ aToken 符号
*/
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
pool = msg.sender;
}
/**
* @notice 修饰符,限制只有借贷池可以调用
*/
modifier onlyPool() {
require(msg.sender == pool, "not pool");
_;
}
/**
* @notice 向用户铸造 aToken
* @dev 只能由借贷池调用
* @param to 接收代币的地址
* @param amount 铸造的代币数量
*/
function mint(address to, uint256 amount) external onlyPool {
_mint(to, amount);
}
/**
* @notice 从用户处销毁 aToken
* @dev 只能由借贷池调用
* @param from 销毁代币的地址
* @param amount 销毁的代币数量
*/
function burn(address from, uint256 amount) external onlyPool {
_burn(from, amount);
}
}
/**
* @title LendingPoolInterestAccrual
* @notice 带利息累积机制的借贷池实现
* @dev 实现类似 Compound 的利息累积机制和准备金机制
*/
contract LendingPoolInterestAccrual {
using SafeERC20 for IERC20;
/// @notice 底层资产代币
IERC20 public immutable asset;
/// @notice 代表存款的 aToken
AToken public immutable aToken;
// 会计变量
/// @notice 总存入的底层代币(包括分配给存款人的利息)
uint256 public totalDeposits;
/// @notice 总未偿还借款
uint256 public totalBorrows;
/// @notice 累计的协议准备金(以底层代币计)
uint256 public totalReserves;
/// @notice 用户的缩放借款:scaledBorrow = actualBorrow * 1e18 / borrowIndex
mapping(address => uint256) public scaledBorrow;
// 利息指数(1e18 精度)
/// @notice 当前借款指数,用于利息计算
uint256 public borrowIndex = 1e18;
/// @notice 当前存款指数,用于利息计算
uint256 public supplyIndex = 1e18;
/// @notice 最后一次利息累积的时间戳
uint256 public lastAccrualTimestamp;
// 利息参数
/// @notice 年化借款利率(1e18 精度,例如 0.10e18 = 10%)
uint256 public annualBorrowRate;
/// @notice 每年秒数,用于利息计算
uint256 public constant SECONDS_PER_YEAR = 31536000;
/// @notice 准备金因子,基点制(1000 = 10%)
uint256 public reserveFactorBps = 1000;
/// @notice 基点分母(10000 = 100%)
uint256 public constant BPS = 10000;
// 事件
event Deposit(address indexed user, uint256 amount, uint256 shares);
event Withdraw(address indexed user, uint256 shares, uint256 amount);
event Borrow(address indexed user, uint256 amount);
event Repay(address indexed user, uint256 amount);
event Accrue(uint256 interestAccrued, uint256 reservesAdded, uint256 newBorrowIndex, uint256 newSupplyIndex);
/**
* @notice 构造函数,初始化借贷池
* @param _asset 底层资产代币
* @param name_ aToken 名称
* @param symbol_ aToken 符号
* @param _annualBorrowRate 年化借款利率(1e18 精度)
* @param _reserveFactorBps 准备金因子,基点制
*/
constructor(
IERC20 _asset,
string memory name_,
string memory symbol_,
uint256 _annualBorrowRate,
uint256 _reserveFactorBps
) {
asset = _asset;
aToken = new AToken(name_, symbol_);
annualBorrowRate = _annualBorrowRate;
reserveFactorBps = _reserveFactorBps;
lastAccrualTimestamp = block.timestamp;
}
// --------------------------------------
// 视图函数
// --------------------------------------
/**
* @notice 获取当前借款余额(包含应计利息)
* @param user 借款人地址
* @return 包含利息的当前借款余额
*/
function borrowBalanceCurrent(address user) public view returns (uint256) {
return (scaledBorrow[user] * borrowIndex) / 1e18;
}
/**
* @notice 获取当前存款余额(包含应计利息)
* @param user 存款人地址
* @return 包含利息的当前存款余额
*/
function supplyBalanceCurrent(address user) public view returns (uint256) {
uint256 shares = aToken.balanceOf(user);
return (shares * supplyIndex) / 1e18;
}
/**
* @notice 计算资金池当前的资金利用率
* @return 资金利用率(1e18 精度)
*/
function utilizationRate() public view returns (uint256) {
if (totalDeposits == 0) return 0;
return (totalBorrows * 1e18) / totalDeposits;
}
/**
* @notice 获取资金池当前可用流动性
* @return 可用于借款的可用流动性
*/
function availableLiquidity() public view returns (uint256) {
return totalDeposits - totalBorrows;
}
// --------------------------------------
// 核心利息累积逻辑
// --------------------------------------
/**
* @notice 内部函数,累积利息并更新指数
* @dev 在任何状态改变操作前都应调用此函数
*/
function _accrueInterest() internal {
uint256 nowTs = block.timestamp;
uint256 delta = nowTs - lastAccrualTimestamp;
if (delta == 0) return;
lastAccrualTimestamp = nowTs;
if (totalBorrows == 0 && totalDeposits == 0) {
// 没有活动,只更新时间戳
return;
}
// 计算每秒借款利率(1e18 精度)
uint256 ratePerSecond = annualBorrowRate / SECONDS_PER_YEAR;
// 计算新的借款指数:borrowIndex *= (1 + ratePerSecond * delta)
uint256 interestFactor = (ratePerSecond * delta); // 1e18 精度
uint256 newBorrowIndex = borrowIndex + (borrowIndex * interestFactor) / 1e18;
// 计算在 delta 时间内产生的利息:totalBorrows * ratePerSecond * delta / 1e18
uint256 interestAccrued = (totalBorrows * ratePerSecond * delta) / 1e18;
// 准备金部分
uint256 reservePortion = (interestAccrued * reserveFactorBps) / BPS;
// 计算每秒存款利率
uint256 utilization = 0;
if (totalDeposits > 0) {
utilization = (totalBorrows * 1e18) / totalDeposits; // 1e18 精度
}
// supplyRatePerSecond = ratePerSecond * utilization * (1 - reserveFactor)
uint256 tmp = (ratePerSecond * utilization) / 1e18; // 1e18 * 1e18 / 1e18 => 1e18 精度
uint256 supplyRatePerSecond = (tmp * (BPS - reserveFactorBps)) / BPS; // 1e18 精度
uint256 newSupplyIndex = supplyIndex + (supplyIndex * supplyRatePerSecond * delta) / 1e18;
// 应用会计变更
// 总借款增加利息部分
totalBorrows += interestAccrued;
// 准备金增加
totalReserves += reservePortion;
// 存款人获得利息减去准备金部分,增加到池子流动性中
uint256 toDepositors = interestAccrued - reservePortion;
if (toDepositors > 0) {
totalDeposits += toDepositors;
}
borrowIndex = newBorrowIndex;
supplyIndex = newSupplyIndex;
emit Accrue(interestAccrued, reservePortion, borrowIndex, supplyIndex);
}
// --------------------------------------
// 用户操作(每个操作都会先更新指数)
// --------------------------------------
/**
* @notice 存款函数
* @param amount 存款数量
*/
function deposit(uint256 amount) external {
require(amount > 0, "invalid amount");
_accrueInterest();
// 转入底层资产
asset.safeTransferFrom(msg.sender, address(this), amount);
// 按当前 supplyIndex 比例铸造份额
// shares = amount * 1e18 / supplyIndex
uint256 shares = (amount * 1e18) / supplyIndex;
aToken.mint(msg.sender, shares);
totalDeposits += amount;
emit Deposit(msg.sender, amount, shares);
}
/**
* @notice 取款函数
* @param shares 取款的份额数量
*/
function withdraw(uint256 shares) external {
require(shares > 0, "invalid shares");
_accrueInterest();
uint256 underlying = (shares * supplyIndex) / 1e18;
require(underlying <= totalDeposits, "insufficient pool");
aToken.burn(msg.sender, shares);
totalDeposits -= underlying;
asset.safeTransfer(msg.sender, underlying);
emit Withdraw(msg.sender, shares, underlying);
}
/**
* @notice 借款函数
* @param amount 借款数量
*/
function borrow(uint256 amount) external {
require(amount > 0, "invalid amount");
_accrueInterest();
// 简单抵押检查:这个教学合约要求存款人=借款人
// 在生产环境中必须评估跨资产抵押;这里我们只要求池子有足够流动性
require(totalDeposits - totalBorrows >= amount, "insufficient liquidity");
// 增加缩放借款:scaled += amount * 1e18 / borrowIndex
uint256 scaledAdd = (amount * 1e18) / borrowIndex;
scaledBorrow[msg.sender] += scaledAdd;
totalBorrows += amount;
asset.safeTransfer(msg.sender, amount);
emit Borrow(msg.sender, amount);
}
/**
* @notice 还款函数
* @param amount 还款数量
*/
function repay(uint256 amount) external {
require(amount > 0, "invalid amount");
_accrueInterest();
uint256 debt = (scaledBorrow[msg.sender] * borrowIndex) / 1e18;
require(debt > 0, "no debt");
uint256 pay = amount;
if (amount > debt) pay = debt;
asset.safeTransferFrom(msg.sender, address(this), pay);
// 更新缩放借款
uint256 newDebt = debt - pay;
if (newDebt == 0) {
scaledBorrow[msg.sender] = 0;
} else {
scaledBorrow[msg.sender] = (newDebt * 1e18) / borrowIndex;
}
totalBorrows -= pay;
// 还款增加池子流动性(我们认为还款直接增加 totalDeposits)
totalDeposits += pay;
emit Repay(msg.sender, pay);
}
// --------------------------------------
// 管理函数
// --------------------------------------
/**
* @notice 设置年化借款利率
* @dev 在生产环境中使用时间锁/治理
* @param _annualBorrowRate 新的年化借款利率
*/
function setAnnualBorrowRate(uint256 _annualBorrowRate) external {
annualBorrowRate = _annualBorrowRate;
}
/**
* @notice 设置准备金因子
* @dev 在生产环境中使用时间锁/治理
* @param _bps 新的准备金因子(基点)
*/
function setReserveFactorBps(uint256 _bps) external {
require(_bps <= BPS, "invalid bps");
reserveFactorBps = _bps;
}
}
测试目的:
reserveFactor
后的利息;totalReserves
正确累积。测试文件:LendingPoolInterestAccrual.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/LendingPoolInterestAccrual.sol";
contract MockERC20 is IERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public override totalSupply;
mapping(address => uint256) public override balanceOf;
mapping(address => mapping(address => uint256)) public override allowance;
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
function transfer(
address to,
uint256 amount
) external override returns (bool) {
require(balanceOf[msg.sender] >= amount, "insuff bal");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(
address spender,
uint256 amount
) external override returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) external override returns (bool) {
require(balanceOf[from] >= amount, "insuff");
require(allowance[from][msg.sender] >= amount, "no allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
totalSupply += amount;
emit Transfer(address(0), to, amount);
}
}
contract InterestAccrualTest is Test {
MockERC20 token;
LendingPoolInterestAccrual pool;
address alice = address(0x1); // 存款人
address bob = address(0x2); // 借款人
address admin = address(0x3); // 管理员
function setUp() public {
token = new MockERC20("MockUSD", "mUSD");
// 年化借款利率 = 10% => 0.10 ether
uint256 annualRate = 0.10 ether;
// 准备金因子 10% (1000 bps)
pool = new LendingPoolInterestAccrual(
IERC20(address(token)),
"aMockUSD",
"aMUSD",
annualRate,
1000
);
// 分配资金
token.mint(alice, 5000 ether);
token.mint(bob, 3000 ether);
token.mint(admin, 1000 ether);
// 批准池子操作
vm.startPrank(alice);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
vm.startPrank(admin);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
}
// 基础功能测试
function testBasicDepositAndWithdraw() public {
// 测试存款
vm.prank(alice);
pool.deposit(1000 ether);
assertEq(pool.totalDeposits(), 1000 ether);
assertEq(token.balanceOf(alice), 4000 ether);
assertEq(
pool.aToken().balanceOf(alice),
(1000 ether * 1e18) / pool.supplyIndex()
);
// 测试取款
uint256 shares = pool.aToken().balanceOf(alice);
vm.prank(alice);
pool.withdraw(shares);
assertEq(pool.totalDeposits(), 0);
assertApproxEqAbs(token.balanceOf(alice), 5000 ether, 1e16); // 允许微小误差
}
function testBasicBorrowAndRepay() public {
// 先存款
vm.prank(alice);
pool.deposit(2000 ether);
// 测试借款
vm.prank(bob);
pool.borrow(1000 ether);
assertEq(pool.totalBorrows(), 1000 ether);
assertEq(token.balanceOf(bob), 4000 ether); // 3000初始 + 1000借款
// 测试还款
vm.prank(bob);
pool.repay(1000 ether);
assertEq(pool.totalBorrows(), 0);
assertEq(pool.borrowBalanceCurrent(bob), 0);
}
// 利息累积测试
function testInterestAccrualOverTime() public {
// Alice 存款 1000
vm.prank(alice);
pool.deposit(1000 ether);
// Bob 借款 500
vm.prank(bob);
pool.borrow(500 ether);
uint256 initialBorrowIndex = pool.borrowIndex();
uint256 initialSupplyIndex = pool.supplyIndex();
// 快进一年
vm.warp(block.timestamp + 365 days);
// 触发利息累积
vm.prank(alice);
pool.deposit(1 ether);
uint256 newBorrowIndex = pool.borrowIndex();
uint256 newSupplyIndex = pool.supplyIndex();
// 验证指数增长
assertGt(newBorrowIndex, initialBorrowIndex);
assertGt(newSupplyIndex, initialSupplyIndex);
// Bob 的债务应该增加约 10%
uint256 bobDebt = pool.borrowBalanceCurrent(bob);
assertApproxEqAbs(bobDebt, 550 ether, 1e17); // 约550,允许1.7%误差
// 验证准备金累积
assertGt(pool.totalReserves(), 0);
}
// 边界测试
function testZeroAmountOperations() public {
// 测试零金额存款
vm.prank(alice);
vm.expectRevert("invalid amount");
pool.deposit(0);
// 测试零金额取款
vm.prank(alice);
vm.expectRevert("invalid shares");
pool.withdraw(0);
// 测试零金额借款
vm.prank(bob);
vm.expectRevert("invalid amount");
pool.borrow(0);
// 测试零金额还款
vm.prank(bob);
vm.expectRevert("invalid amount");
pool.repay(0);
}
function testInsufficientLiquidity() public {
// Alice 存款 1000
vm.prank(alice);
pool.deposit(1000 ether);
// Bob 尝试借超过可用流动性的金额
vm.prank(bob);
vm.expectRevert("insufficient liquidity");
pool.borrow(1001 ether);
}
function testInsufficientDeposits() public {
// Alice 存款 1000
vm.prank(alice);
pool.deposit(1000 ether);
// Alice 尝试取超过她存款的金额
uint256 excessShares = (1001 ether * 1e18) / pool.supplyIndex();
vm.prank(alice);
vm.expectRevert("insufficient pool");
pool.withdraw(excessShares);
}
function testNoDebtRepay() public {
// Bob 尝试还款但没有债务
vm.prank(bob);
vm.expectRevert("no debt");
pool.repay(100 ether);
}
// 多用户场景测试
function testMultipleUsers() public {
// 多个用户存款
vm.prank(alice);
pool.deposit(1000 ether);
vm.prank(admin);
pool.deposit(500 ether);
assertEq(pool.totalDeposits(), 1500 ether);
// 多个用户借款
vm.prank(bob);
pool.borrow(800 ether);
assertEq(pool.totalBorrows(), 800 ether);
assertEq(pool.availableLiquidity(), 700 ether);
// 验证利用率
uint256 utilization = pool.utilizationRate();
uint256 expected = (uint256(800 ether) * 1e18) / uint256(1500 ether);
assertApproxEqAbs(utilization, expected, 1e16);
}
// 超额还款测试
function testOverRepayment() public {
// 设置
vm.prank(alice);
pool.deposit(1000 ether);
vm.prank(bob);
pool.borrow(500 ether);
// Bob 尝试超额还款
vm.prank(bob);
pool.repay(600 ether); // 只应收取实际债务金额
assertEq(pool.totalBorrows(), 0);
assertEq(pool.borrowBalanceCurrent(bob), 0);
// Bob 应该只被扣除实际债务金额(3000 初始 + 500 借款 - 500 还款 = 3000)
assertApproxEqAbs(token.balanceOf(bob), 3000 ether, 1e16);
}
// 管理员功能测试
function testAdminFunctions() public {
// 测试设置借款利率
uint256 newRate = 0.15 ether; // 15%
pool.setAnnualBorrowRate(newRate);
assertEq(pool.annualBorrowRate(), newRate);
// 测试设置准备金因子
uint256 newReserveFactor = 2000; // 20%
pool.setReserveFactorBps(newReserveFactor);
assertEq(pool.reserveFactorBps(), newReserveFactor);
// 测试无效的准备金因子
vm.expectRevert("invalid bps");
pool.setReserveFactorBps(10001); // 超过100%
}
// 极端情况测试
function testHighUtilization() public {
// Alice 存款
vm.prank(alice);
pool.deposit(1000 ether);
// Bob 借几乎全部资金
vm.prank(bob);
pool.borrow(999 ether);
// 验证高利用率
uint256 utilization = pool.utilizationRate();
assertGt(utilization, 0.99 ether); // 利用率 > 99%
// 快进时间累积利息
vm.warp(block.timestamp + 30 days);
// 触发利息累积
vm.prank(alice);
pool.deposit(1 ether);
// 验证利息正确累积
assertGt(pool.totalBorrows(), 999 ether);
assertGt(pool.totalDeposits(), 1000 ether);
}
// 视图函数测试
function testViewFunctions() public {
// 初始状态检查
assertEq(pool.utilizationRate(), 0);
assertEq(pool.availableLiquidity(), 0);
// 存款后检查
vm.prank(alice);
pool.deposit(1000 ether);
assertEq(pool.utilizationRate(), 0);
assertEq(pool.availableLiquidity(), 1000 ether);
assertEq(pool.supplyBalanceCurrent(alice), 1000 ether);
// 借款后检查
vm.prank(bob);
pool.borrow(500 ether);
uint256 utilization = pool.utilizationRate();
assertApproxEqAbs(utilization, 0.5 ether, 1e16); // 约50%
assertEq(pool.availableLiquidity(), 500 ether);
assertEq(pool.borrowBalanceCurrent(bob), 500 ether);
}
}
重要测试说明:
vm.warp()
快速推进时间并通过一次交易(deposit/repay)触发 _accrueInterest()
;实际生产中可由 keeper 定期调用全局 accrue()
或在每次用户交互时调用。assertApproxEqAbs
用于允许少量整数舍入误差(微小的 rounding)。执行测试:
➜ defi git:(main) ✗ forge test --match-path test/LendingPoolInterestAccrual.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 513.88ms
Compiler run successful!
Ran 12 tests for test/LendingPoolInterestAccrual.t.sol:InterestAccrualTest
[PASS] testAdminFunctions() (gas: 23968)
[PASS] testBasicBorrowAndRepay() (gas: 178727)
[PASS] testBasicDepositAndWithdraw() (gas: 126616)
[PASS] testHighUtilization() (gas: 260831)
[PASS] testInsufficientDeposits() (gas: 137902)
[PASS] testInsufficientLiquidity() (gas: 140535)
[PASS] testInterestAccrualOverTime() (gas: 265547)
[PASS] testMultipleUsers() (gas: 251870)
[PASS] testNoDebtRepay() (gas: 19149)
[PASS] testOverRepayment() (gas: 178234)
[PASS] testViewFunctions() (gas: 212301)
[PASS] testZeroAmountOperations() (gas: 22277)
Suite result: ok. 12 passed; 0 failed; 0 skipped; finished in 11.49ms (9.09ms CPU time)
Ran 1 test suite in 154.45ms (11.49ms CPU time): 12 tests passed, 0 failed, 0 skipped (12 total tests)
borrowIndex
,存款人按 supplyIndex
)。accrue
的触发策略都直接影响协议经济行为与 gas 成本。InterestRateModel
对接:让 annualBorrowRate
不再固定,而是从 InterestRateModel.borrowRate(totalDeposits, totalBorrows)
获取(注意精度换算)。写测试验证动态利率生效。accrue
的调用策略:实现 accrueIfNeeded(address user)
,仅在必要时(如 borrow/repay)触发或由 keeper 定期触发并计费。比较每次触发 vs keeper 模式的 gas 差异。mapping(address => Market)
),并保证 getTotalBorrowValue
与 getTotalSupplyValue
的一致性。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。