2025-12-18 13:07:35 +08:00
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
|
|
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
|
|
|
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
|
|
|
|
|
|
|
|
|
|
import "./LendingStorage.sol";
|
|
|
|
|
|
import "./LendingMath.sol";
|
|
|
|
|
|
import "./interfaces/ILending.sol";
|
|
|
|
|
|
import "./interfaces/IPriceFeed.sol";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @title Lending
|
|
|
|
|
|
* @notice 借贷池核心合约
|
|
|
|
|
|
*/
|
|
|
|
|
|
contract Lending is
|
|
|
|
|
|
ILending,
|
|
|
|
|
|
LendingStorage,
|
|
|
|
|
|
UUPSUpgradeable,
|
|
|
|
|
|
OwnableUpgradeable,
|
|
|
|
|
|
PausableUpgradeable,
|
|
|
|
|
|
ReentrancyGuardUpgradeable
|
|
|
|
|
|
{
|
|
|
|
|
|
using SafeERC20 for IERC20;
|
|
|
|
|
|
|
|
|
|
|
|
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
_disableInitializers();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 初始化函数
|
|
|
|
|
|
* @param config 市场配置
|
|
|
|
|
|
*/
|
|
|
|
|
|
function initialize(Configuration calldata config) external initializer {
|
|
|
|
|
|
__UUPSUpgradeable_init();
|
|
|
|
|
|
__Ownable_init(msg.sender);
|
|
|
|
|
|
__Pausable_init();
|
|
|
|
|
|
__ReentrancyGuard_init();
|
|
|
|
|
|
|
|
|
|
|
|
// 设置基础配置
|
|
|
|
|
|
baseToken = config.baseToken;
|
|
|
|
|
|
baseTokenPriceFeed = config.baseTokenPriceFeed;
|
|
|
|
|
|
|
|
|
|
|
|
// 常量:一年的秒数
|
|
|
|
|
|
uint256 SECONDS_PER_YEAR = 365 * 24 * 60 * 60; // 31,536,000
|
|
|
|
|
|
|
2025-12-19 10:54:52 +08:00
|
|
|
|
// 设置利率参数
|
2025-12-18 13:07:35 +08:00
|
|
|
|
supplyKink = config.supplyKink;
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeLow = uint64(config.supplyPerYearInterestRateSlopeLow / SECONDS_PER_YEAR);
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeHigh = uint64(config.supplyPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR);
|
|
|
|
|
|
supplyPerSecondInterestRateBase = uint64(config.supplyPerYearInterestRateBase / SECONDS_PER_YEAR);
|
|
|
|
|
|
|
|
|
|
|
|
borrowKink = config.borrowKink;
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeLow = uint64(config.borrowPerYearInterestRateSlopeLow / SECONDS_PER_YEAR);
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeHigh = uint64(config.borrowPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR);
|
|
|
|
|
|
borrowPerSecondInterestRateBase = uint64(config.borrowPerYearInterestRateBase / SECONDS_PER_YEAR);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置其他参数
|
|
|
|
|
|
storeFrontPriceFactor = config.storeFrontPriceFactor;
|
|
|
|
|
|
trackingIndexScale = config.trackingIndexScale;
|
|
|
|
|
|
baseBorrowMin = config.baseBorrowMin;
|
|
|
|
|
|
targetReserves = config.targetReserves;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化利息累计因子
|
|
|
|
|
|
supplyIndex = 1e18;
|
|
|
|
|
|
borrowIndex = 1e18;
|
|
|
|
|
|
lastAccrualTime = block.timestamp;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置抵押资产配置
|
|
|
|
|
|
for (uint i = 0; i < config.assetConfigs.length; i++) {
|
|
|
|
|
|
AssetConfig memory assetConfig = config.assetConfigs[i];
|
|
|
|
|
|
|
|
|
|
|
|
// 验证参数合法性(必须 < 1)
|
2025-12-19 10:54:52 +08:00
|
|
|
|
if(assetConfig.liquidationFactor >= 1e18) revert InvalidLiquidationFactor();
|
|
|
|
|
|
if(assetConfig.borrowCollateralFactor >= 1e18) revert InvalidBorrowCollateralFactor();
|
|
|
|
|
|
if(assetConfig.liquidateCollateralFactor >= 1e18) revert InvalidLiquidateCollateralFactor();
|
2025-12-18 13:07:35 +08:00
|
|
|
|
|
|
|
|
|
|
assetConfigs[assetConfig.asset] = assetConfig;
|
|
|
|
|
|
assetList.push(assetConfig.asset);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @dev 授权升级函数 - 只有 owner 可以升级
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 暂停合约
|
|
|
|
|
|
*/
|
|
|
|
|
|
function pause() external onlyOwner {
|
|
|
|
|
|
_pause();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 恢复合约
|
|
|
|
|
|
*/
|
|
|
|
|
|
function unpause() external onlyOwner {
|
|
|
|
|
|
_unpause();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 计提利息
|
|
|
|
|
|
*/
|
|
|
|
|
|
function accrueInterest() public {
|
|
|
|
|
|
uint256 timeElapsed = block.timestamp - lastAccrualTime;
|
|
|
|
|
|
if (timeElapsed == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际的 totalSupply 和 totalBorrow(含利息)
|
|
|
|
|
|
// 注意:totalSupplyBase 和 totalBorrowBase 都是正数本金
|
|
|
|
|
|
// supplyIndex 用于存款,borrowIndex 用于借款
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
|
|
|
|
|
|
uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算供应利率和借款利率(每秒利率)
|
|
|
|
|
|
uint64 supplyRate = LendingMath.getSupplyRate(
|
|
|
|
|
|
utilization,
|
|
|
|
|
|
supplyKink,
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeLow,
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeHigh,
|
|
|
|
|
|
supplyPerSecondInterestRateBase
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
uint64 borrowRate = LendingMath.getBorrowRate(
|
|
|
|
|
|
utilization,
|
|
|
|
|
|
borrowKink,
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeLow,
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeHigh,
|
|
|
|
|
|
borrowPerSecondInterestRateBase
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新利息累计因子(使用每秒利率,计算更精确且 Gas 更低)
|
|
|
|
|
|
supplyIndex = LendingMath.accrueInterest(supplyIndex, supplyRate, timeElapsed);
|
|
|
|
|
|
borrowIndex = LendingMath.accrueInterest(borrowIndex, borrowRate, timeElapsed);
|
|
|
|
|
|
|
|
|
|
|
|
lastAccrualTime = block.timestamp;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 存入基础资产
|
|
|
|
|
|
*/
|
|
|
|
|
|
function supply(uint256 amount) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
|
|
|
|
|
|
IERC20(baseToken).safeTransferFrom(msg.sender, address(this), amount);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户当前本金
|
|
|
|
|
|
UserBasic memory user = userBasic[msg.sender];
|
|
|
|
|
|
int104 oldPrincipal = user.principal;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前实际余额(含利息)
|
|
|
|
|
|
uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算新余额(增加存款)
|
|
|
|
|
|
int256 newBalance = oldBalance + int256(amount);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为新本金(可能从借款变为存款)
|
|
|
|
|
|
uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算还款和存款金额
|
|
|
|
|
|
(uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新全局状态
|
|
|
|
|
|
totalBorrowBase -= repayAmount;
|
|
|
|
|
|
totalSupplyBase += supplyAmount;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户本金
|
|
|
|
|
|
userBasic[msg.sender].principal = newPrincipal;
|
|
|
|
|
|
|
|
|
|
|
|
emit Supply(msg.sender, msg.sender, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 取出基础资产(如果余额不足会自动借款)
|
2025-12-19 17:14:57 +08:00
|
|
|
|
* @dev 如果用户余额不足,会自动借款,借款金额为 amount,借款利率为 borrowRate,借款期限为 borrowPeriod
|
2025-12-18 13:07:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function withdraw(uint256 amount) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户当前本金
|
|
|
|
|
|
UserBasic memory user = userBasic[msg.sender];
|
|
|
|
|
|
int104 oldPrincipal = user.principal;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前实际余额(含利息)
|
|
|
|
|
|
uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算新余额
|
|
|
|
|
|
int256 newBalance = oldBalance - int256(amount);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为新本金
|
|
|
|
|
|
uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算提取和借款金额
|
|
|
|
|
|
(uint104 withdrawAmount, uint104 borrowAmount) = LendingMath.withdrawAndBorrowAmount(oldPrincipal, newPrincipal);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新全局状态
|
|
|
|
|
|
totalSupplyBase -= withdrawAmount;
|
|
|
|
|
|
totalBorrowBase += borrowAmount;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户本金
|
|
|
|
|
|
userBasic[msg.sender].principal = newPrincipal;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果变成负余额(借款),检查抵押品
|
|
|
|
|
|
if (newBalance < 0) {
|
|
|
|
|
|
if (uint256(-newBalance) < baseBorrowMin) revert BorrowTooSmall();
|
|
|
|
|
|
if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IERC20(baseToken).safeTransfer(msg.sender, amount);
|
|
|
|
|
|
|
|
|
|
|
|
emit Withdraw(msg.sender, msg.sender, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 存入抵押品
|
2025-12-19 17:14:57 +08:00
|
|
|
|
* @dev 由于不涉及债务计算,存入抵押品反而会让账户更安全,所以不用更新利息因子
|
2025-12-18 13:07:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function supplyCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
AssetConfig memory config = assetConfigs[asset];
|
|
|
|
|
|
if (config.asset == address(0)) revert Unauthorized();
|
|
|
|
|
|
|
|
|
|
|
|
uint256 newTotal = userCollateral[msg.sender][asset] + amount;
|
|
|
|
|
|
if (newTotal > config.supplyCap) revert SupplyCapExceeded();
|
|
|
|
|
|
|
|
|
|
|
|
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
|
|
|
|
|
|
|
|
|
|
|
|
userCollateral[msg.sender][asset] += amount;
|
|
|
|
|
|
|
|
|
|
|
|
emit SupplyCollateral(msg.sender, msg.sender, asset, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 取出抵押品
|
|
|
|
|
|
*/
|
|
|
|
|
|
function withdrawCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
|
|
|
|
|
|
if (userCollateral[msg.sender][asset] < amount) revert InsufficientBalance();
|
|
|
|
|
|
|
|
|
|
|
|
userCollateral[msg.sender][asset] -= amount;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否仍有足够的抵押品(如果有债务)
|
|
|
|
|
|
int104 principal = userBasic[msg.sender].principal;
|
|
|
|
|
|
if (principal < 0) {
|
|
|
|
|
|
if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IERC20(asset).safeTransfer(msg.sender, amount);
|
|
|
|
|
|
|
|
|
|
|
|
emit WithdrawCollateral(msg.sender, msg.sender, asset, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 借款
|
2025-12-19 10:54:52 +08:00
|
|
|
|
* @dev baseBorrowMin 是用户借款的最小金额,如果用户借款后,余额小于 baseBorrowMin(由正数变为负数同理),则抛出 BorrowTooSmall 错误
|
2025-12-18 13:07:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function borrow(uint256 amount) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户当前本金
|
|
|
|
|
|
UserBasic memory user = userBasic[msg.sender];
|
|
|
|
|
|
int104 oldPrincipal = user.principal;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前实际余额(含利息)
|
|
|
|
|
|
uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算新余额(减去借款额)
|
|
|
|
|
|
int256 newBalance = oldBalance - int256(amount);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查最小借款额
|
|
|
|
|
|
if (newBalance < 0 && uint256(-newBalance) < baseBorrowMin) revert BorrowTooSmall();
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为新本金(新状态可能从存款变为借款)
|
|
|
|
|
|
uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
|
|
|
|
|
|
int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算提取和借款金额
|
|
|
|
|
|
(uint104 withdrawAmount, uint104 borrowAmount) = LendingMath.withdrawAndBorrowAmount(oldPrincipal, newPrincipal);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新全局状态
|
|
|
|
|
|
totalSupplyBase -= withdrawAmount;
|
|
|
|
|
|
totalBorrowBase += borrowAmount;
|
|
|
|
|
|
|
2025-12-19 10:54:52 +08:00
|
|
|
|
// 更新用户本金,方便检查更新后的用户本金是否大于还是小于抵押品价值
|
2025-12-18 13:07:35 +08:00
|
|
|
|
userBasic[msg.sender].principal = newPrincipal;
|
|
|
|
|
|
|
2025-12-19 10:54:52 +08:00
|
|
|
|
// 检查抵押品是否充足
|
2025-12-18 13:07:35 +08:00
|
|
|
|
if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
|
|
|
|
|
|
|
|
|
|
|
|
IERC20(baseToken).safeTransfer(msg.sender, amount);
|
|
|
|
|
|
|
|
|
|
|
|
emit Withdraw(msg.sender, msg.sender, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 清算不良债务(内部实现)
|
2025-12-19 10:54:52 +08:00
|
|
|
|
* @dev 当用户抵押品由于乘以liquidateCollateralFactor后,小于债务价值时,会进行清算,清算后,如果实际抵押品价值大于债务价值,则将差额部分作为用户本金(本金以baseToken显示),否则将差额部分作为坏账,由协议承担
|
2025-12-18 13:07:35 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function _absorbInternal(address absorber, address borrower) internal {
|
|
|
|
|
|
if (!isLiquidatable(borrower)) revert NotLiquidatable();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户当前本金
|
|
|
|
|
|
UserBasic memory user = userBasic[borrower];
|
|
|
|
|
|
int104 oldPrincipal = user.principal;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前实际余额(含利息累计的债务)
|
|
|
|
|
|
int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, borrowIndex);
|
|
|
|
|
|
if (oldBalance >= 0) revert NotLiquidatable();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算所有抵押品的总价值(按 liquidationFactor 折扣)
|
|
|
|
|
|
uint256 basePrice = IPriceFeed(baseTokenPriceFeed).getPrice();
|
|
|
|
|
|
uint256 totalCollateralValue = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (uint i = 0; i < assetList.length; i++) {
|
|
|
|
|
|
address asset = assetList[i];
|
|
|
|
|
|
uint256 collateralAmount = userCollateral[borrower][asset];
|
|
|
|
|
|
|
|
|
|
|
|
if (collateralAmount > 0) {
|
|
|
|
|
|
AssetConfig memory assetConfig = assetConfigs[asset];
|
|
|
|
|
|
uint256 assetPrice = IPriceFeed(assetConfig.priceFeed).getPrice();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算抵押品价值(USD,8位精度)
|
|
|
|
|
|
uint256 assetScale = 10 ** assetConfig.decimals;
|
|
|
|
|
|
uint256 collateralValueUSD = (collateralAmount * assetPrice) / assetScale;
|
|
|
|
|
|
|
|
|
|
|
|
// 应用 liquidationFactor 折扣
|
|
|
|
|
|
uint256 discountedValue = (collateralValueUSD * assetConfig.liquidationFactor) / 1e18;
|
|
|
|
|
|
totalCollateralValue += discountedValue;
|
|
|
|
|
|
|
|
|
|
|
|
// 将抵押品转移到清算库存
|
|
|
|
|
|
userCollateral[borrower][asset] = 0;
|
|
|
|
|
|
collateralReserves[asset] += collateralAmount;
|
|
|
|
|
|
|
|
|
|
|
|
// 发射抵押品吸收事件
|
|
|
|
|
|
emit AbsorbCollateral(absorber, borrower, asset, collateralAmount, collateralValueUSD);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将抵押品价值转换为 baseToken 数量
|
|
|
|
|
|
uint256 baseScale = 10 ** IERC20Metadata(baseToken).decimals();
|
|
|
|
|
|
uint256 collateralInBase = (totalCollateralValue * baseScale) / basePrice;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算新余额:oldBalance(负数)+ 抵押品价值
|
|
|
|
|
|
int256 newBalance = oldBalance + int256(collateralInBase);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果新余额仍为负,强制归零(坏账由协议承担)
|
|
|
|
|
|
if (newBalance < 0) {
|
|
|
|
|
|
newBalance = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为新本金
|
|
|
|
|
|
int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, supplyIndex);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户本金
|
|
|
|
|
|
userBasic[borrower].principal = newPrincipal;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算偿还和供应金额
|
|
|
|
|
|
(uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
|
|
|
|
|
|
|
2025-12-19 17:14:57 +08:00
|
|
|
|
// 更新全局状态(储备金通过减少 totalBorrowBase 和增加 totalSupplyBase 来承担坏账)
|
2025-12-18 13:07:35 +08:00
|
|
|
|
totalSupplyBase += supplyAmount;
|
|
|
|
|
|
totalBorrowBase -= repayAmount;
|
|
|
|
|
|
|
2025-12-19 17:14:57 +08:00
|
|
|
|
// 计算协议承担的坏账部分
|
|
|
|
|
|
// 坏账 = 用户债务 - 抵押品价值(当抵押品不足时)
|
|
|
|
|
|
uint256 basePaidOut = 0;
|
|
|
|
|
|
if (int256(collateralInBase) < -oldBalance) {
|
|
|
|
|
|
// 抵押品不足以覆盖债务,差额由协议储备金承担
|
|
|
|
|
|
basePaidOut = uint256(-oldBalance) - collateralInBase;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果 collateralInBase >= -oldBalance,说明抵押品足够,无坏账
|
|
|
|
|
|
|
2025-12-18 13:07:35 +08:00
|
|
|
|
uint256 valueOfBasePaidOut = (basePaidOut * basePrice) / baseScale;
|
|
|
|
|
|
|
|
|
|
|
|
// 发射债务吸收事件
|
|
|
|
|
|
emit AbsorbDebt(absorber, borrower, basePaidOut, valueOfBasePaidOut);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 清算不良债务(单个)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function absorb(address borrower) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
_absorbInternal(msg.sender, borrower);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 批量清算不良债务
|
|
|
|
|
|
*/
|
|
|
|
|
|
function absorbMultiple(address absorber, address[] calldata accounts) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
accrueInterest();
|
|
|
|
|
|
for (uint i = 0; i < accounts.length; ) {
|
|
|
|
|
|
_absorbInternal(absorber, accounts[i]);
|
|
|
|
|
|
unchecked { i++; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 购买清算后的抵押品
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buyCollateral(
|
|
|
|
|
|
address asset,
|
|
|
|
|
|
uint256 minAmount,
|
|
|
|
|
|
uint256 baseAmount,
|
|
|
|
|
|
address recipient
|
|
|
|
|
|
) external override nonReentrant whenNotPaused {
|
|
|
|
|
|
if (collateralReserves[asset] == 0) revert InsufficientBalance();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算可购买的抵押品数量
|
|
|
|
|
|
uint256 collateralAmount = quoteCollateral(asset, baseAmount);
|
|
|
|
|
|
|
|
|
|
|
|
// 验证数量
|
|
|
|
|
|
if (collateralAmount < minAmount) revert InsufficientBalance();
|
|
|
|
|
|
if (collateralAmount > collateralReserves[asset]) revert InsufficientBalance();
|
|
|
|
|
|
|
|
|
|
|
|
// 检查储备金是否充足(如果已达到目标,不再出售抵押品)
|
|
|
|
|
|
uint256 balance = IERC20(baseToken).balanceOf(address(this));
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
int256 currentReserves = int256(balance) - int256(totalSupply) + int256(totalBorrow);
|
|
|
|
|
|
if (currentReserves >= 0 && uint256(currentReserves) >= targetReserves) {
|
|
|
|
|
|
revert InsufficientBalance(); // 储备金充足,无需出售
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 收取清算人支付的资金
|
|
|
|
|
|
IERC20(baseToken).safeTransferFrom(msg.sender, address(this), baseAmount);
|
|
|
|
|
|
|
|
|
|
|
|
// 抵押品出库
|
|
|
|
|
|
collateralReserves[asset] -= collateralAmount;
|
|
|
|
|
|
|
|
|
|
|
|
// 转账抵押品到指定接收人
|
|
|
|
|
|
IERC20(asset).safeTransfer(recipient, collateralAmount);
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:收入会自动体现在 getReserves() 中,因为 balance 增加了
|
|
|
|
|
|
emit BuyCollateral(msg.sender, asset, baseAmount, collateralAmount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 计算支付指定baseAmount可购买的抵押品数量
|
|
|
|
|
|
*/
|
|
|
|
|
|
function quoteCollateral(address asset, uint256 baseAmount) public view override returns (uint256) {
|
|
|
|
|
|
AssetConfig memory assetConfig = assetConfigs[asset];
|
|
|
|
|
|
|
|
|
|
|
|
uint256 assetPrice = IPriceFeed(assetConfig.priceFeed).getPrice();
|
|
|
|
|
|
uint256 basePrice = IPriceFeed(baseTokenPriceFeed).getPrice();
|
|
|
|
|
|
|
2025-12-19 10:54:52 +08:00
|
|
|
|
// 计算折扣率
|
2025-12-18 13:07:35 +08:00
|
|
|
|
// discountFactor = storeFrontPriceFactor * (FACTOR_SCALE - liquidationFactor) / FACTOR_SCALE
|
|
|
|
|
|
uint256 FACTOR_SCALE = 1e18;
|
|
|
|
|
|
uint256 discountFactor = (storeFrontPriceFactor * (FACTOR_SCALE - assetConfig.liquidationFactor)) / FACTOR_SCALE;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算折扣后的资产价格
|
|
|
|
|
|
// assetPriceDiscounted = assetPrice * (FACTOR_SCALE - discountFactor) / FACTOR_SCALE
|
|
|
|
|
|
uint256 assetPriceDiscounted = (assetPrice * (FACTOR_SCALE - discountFactor)) / FACTOR_SCALE;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算可购买的抵押品数量
|
|
|
|
|
|
// 公式:(basePrice * baseAmount * assetScale) / (assetPriceDiscounted * baseScale)
|
|
|
|
|
|
uint256 baseScale = 10 ** uint256(IERC20Metadata(baseToken).decimals());
|
|
|
|
|
|
uint256 assetScale = 10 ** uint256(assetConfig.decimals);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用中间变量分步计算,避免潜在的溢出
|
|
|
|
|
|
// 先计算分子和分母,再进行除法
|
|
|
|
|
|
return (basePrice * baseAmount * assetScale) / (assetPriceDiscounted * baseScale);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 检查账户偿付能力
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _isSolvent(address account) internal view returns (bool) {
|
|
|
|
|
|
int104 principal = userBasic[account].principal;
|
|
|
|
|
|
if (principal >= 0) return true;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际债务(含利息)- 使用 borrowIndex
|
|
|
|
|
|
int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
|
|
|
|
|
|
uint256 debt = uint256(-balance);
|
|
|
|
|
|
|
|
|
|
|
|
// 将 debt 转换为美元价值(使用 baseToken 价格)
|
|
|
|
|
|
uint256 basePrice = IPriceFeed(baseTokenPriceFeed).getPrice();
|
|
|
|
|
|
uint256 baseDecimals = IERC20Metadata(baseToken).decimals();
|
|
|
|
|
|
uint256 debtValue = (debt * basePrice) / (10 ** baseDecimals);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算借款能力(抵押品价值已经在 _getCollateralValue 中应用了借款系数)
|
|
|
|
|
|
uint256 borrowCapacity = _getCollateralValue(account);
|
|
|
|
|
|
|
|
|
|
|
|
// 比较:借款能力 >= 债务价值
|
|
|
|
|
|
return borrowCapacity >= debtValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 计算账户抵押品总价值
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _getCollateralValue(address account) internal view returns (uint256) {
|
|
|
|
|
|
uint256 totalValue = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (uint i = 0; i < assetList.length; i++) {
|
|
|
|
|
|
address asset = assetList[i];
|
|
|
|
|
|
uint256 amount = userCollateral[account][asset];
|
|
|
|
|
|
if (amount > 0) {
|
|
|
|
|
|
AssetConfig memory config = assetConfigs[asset];
|
|
|
|
|
|
uint256 price = IPriceFeed(config.priceFeed).getPrice();
|
|
|
|
|
|
uint256 value = LendingMath.getCollateralValue(amount, price, config.decimals);
|
|
|
|
|
|
totalValue += (value * config.borrowCollateralFactor) / 1e18;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return totalValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== View Functions ==========
|
|
|
|
|
|
|
|
|
|
|
|
function getBalance(address account) external view override returns (int256) {
|
|
|
|
|
|
int104 principal = userBasic[account].principal;
|
|
|
|
|
|
// 使用 supplyIndex 计算实际余额(含利息)
|
|
|
|
|
|
return LendingMath.principalToBalance(principal, supplyIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function balanceOf(address account) external view override returns (uint256) {
|
|
|
|
|
|
int104 principal = userBasic[account].principal;
|
|
|
|
|
|
if (principal <= 0) return 0;
|
|
|
|
|
|
// 只返回正余额(存款)
|
|
|
|
|
|
return uint256(LendingMath.principalToBalance(principal, supplyIndex));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function borrowBalanceOf(address account) external view override returns (uint256) {
|
|
|
|
|
|
int104 principal = userBasic[account].principal;
|
|
|
|
|
|
if (principal >= 0) return 0;
|
|
|
|
|
|
// 只返回负余额(借款),转为正数
|
|
|
|
|
|
int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
|
|
|
|
|
|
return uint256(-balance);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getCollateral(address account, address asset) external view override returns (uint256) {
|
|
|
|
|
|
return userCollateral[account][asset];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isLiquidatable(address account) public view override returns (bool) {
|
|
|
|
|
|
int104 principal = userBasic[account].principal;
|
|
|
|
|
|
if (principal >= 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际债务(含利息)
|
|
|
|
|
|
int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
|
|
|
|
|
|
uint256 debt = uint256(-balance);
|
|
|
|
|
|
|
|
|
|
|
|
// 将 debt 转换为美元价值(使用 baseToken 价格和 price feed 精度)
|
|
|
|
|
|
uint256 basePrice = IPriceFeed(baseTokenPriceFeed).getPrice();
|
|
|
|
|
|
uint256 baseDecimals = IERC20Metadata(baseToken).decimals();
|
|
|
|
|
|
uint256 debtValue = (debt * basePrice) / (10 ** baseDecimals);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算抵押品总价值(清算阈值)
|
|
|
|
|
|
uint256 collateralValue = 0;
|
|
|
|
|
|
for (uint i = 0; i < assetList.length; i++) {
|
|
|
|
|
|
address asset = assetList[i];
|
|
|
|
|
|
uint256 amount = userCollateral[account][asset];
|
|
|
|
|
|
if (amount > 0) {
|
|
|
|
|
|
AssetConfig memory config = assetConfigs[asset];
|
|
|
|
|
|
uint256 price = IPriceFeed(config.priceFeed).getPrice();
|
|
|
|
|
|
uint256 value = LendingMath.getCollateralValue(amount, price, config.decimals);
|
|
|
|
|
|
collateralValue += (value * config.liquidateCollateralFactor) / 1e18;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 比较:债务价值 > 抵押品清算阈值价值
|
|
|
|
|
|
return debtValue > collateralValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTotalSupply() external view returns (uint256) {
|
|
|
|
|
|
return (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getTotalBorrow() external view returns (uint256) {
|
|
|
|
|
|
return (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getCollateralReserves(address asset) external view override returns (uint256) {
|
|
|
|
|
|
return collateralReserves[asset];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getReserves() external view override returns (int256) {
|
|
|
|
|
|
// 计算实际总供应和总借款(含利息)
|
|
|
|
|
|
uint256 balance = IERC20(baseToken).balanceOf(address(this));
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
|
|
|
|
|
|
// reserves = balance - totalSupply + totalBorrow
|
|
|
|
|
|
return int256(balance) - int256(totalSupply) + int256(totalBorrow);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getUtilization() external view override returns (uint256) {
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
return LendingMath.getUtilization(totalSupply, totalBorrow);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getSupplyRate() external view override returns (uint64) {
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
|
|
|
|
|
|
uint64 perSecondRate = LendingMath.getSupplyRate(
|
|
|
|
|
|
utilization,
|
|
|
|
|
|
supplyKink,
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeLow,
|
|
|
|
|
|
supplyPerSecondInterestRateSlopeHigh,
|
|
|
|
|
|
supplyPerSecondInterestRateBase
|
|
|
|
|
|
);
|
|
|
|
|
|
// 转换为年化利率(APY)
|
|
|
|
|
|
return perSecondRate * 31536000; // SECONDS_PER_YEAR
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice 提取协议储备金(仅 owner)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function withdrawReserves(address to, uint256 amount) external override onlyOwner nonReentrant {
|
|
|
|
|
|
uint256 balance = IERC20(baseToken).balanceOf(address(this));
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
int256 currentReserves = int256(balance) - int256(totalSupply) + int256(totalBorrow);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查储备金是否充足
|
|
|
|
|
|
if (currentReserves < 0 || amount > uint256(currentReserves)) {
|
|
|
|
|
|
revert InsufficientReserves();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转账储备金
|
|
|
|
|
|
IERC20(baseToken).safeTransfer(to, amount);
|
|
|
|
|
|
|
|
|
|
|
|
emit WithdrawReserves(to, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getBorrowRate() external view override returns (uint64) {
|
|
|
|
|
|
uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
|
|
|
|
|
|
uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
|
|
|
|
|
|
uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
|
|
|
|
|
|
uint64 perSecondRate = LendingMath.getBorrowRate(
|
|
|
|
|
|
utilization,
|
|
|
|
|
|
borrowKink,
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeLow,
|
|
|
|
|
|
borrowPerSecondInterestRateSlopeHigh,
|
|
|
|
|
|
borrowPerSecondInterestRateBase
|
|
|
|
|
|
);
|
|
|
|
|
|
// 转换为年化利率(APY)
|
|
|
|
|
|
return perSecondRate * 31536000; // SECONDS_PER_YEAR
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|