// 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/IYTLendingPriceFeed.sol"; contract Lending is ILending, LendingStorage, UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(Configuration calldata config) external initializer { __UUPSUpgradeable_init(); __Ownable_init(msg.sender); __Pausable_init(); __ReentrancyGuard_init(); baseToken = config.baseToken; lendingPriceSource = config.lendingPriceSource; uint64 SECONDS_PER_YEAR = 365 * 24 * 60 * 60; 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; 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]; if(assetConfig.liquidationFactor >= 1e18) revert InvalidLiquidationFactor(); if(assetConfig.borrowCollateralFactor >= 1e18) revert InvalidBorrowCollateralFactor(); if(assetConfig.liquidateCollateralFactor >= 1e18) revert InvalidLiquidateCollateralFactor(); assetConfigs[assetConfig.asset] = assetConfig; assetList.push(assetConfig.asset); } } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } function accruedInterestIndices(uint256 timeElapsed) internal view returns (uint256, uint256) { uint256 newSupplyIndex = supplyIndex; uint256 newBorrowIndex = borrowIndex; if (timeElapsed > 0) { 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 ); newSupplyIndex = LendingMath.accrueInterest(supplyIndex, supplyRate, timeElapsed); newBorrowIndex = LendingMath.accrueInterest(borrowIndex, borrowRate, timeElapsed); } return (newSupplyIndex, newBorrowIndex); } function accrueInterest() public { uint256 timeElapsed = block.timestamp - lastAccrualTime; if (timeElapsed == 0) return; (supplyIndex, borrowIndex) = accruedInterestIndices(timeElapsed); lastAccrualTime = block.timestamp; } function supply(uint256 amount) external override nonReentrant whenNotPaused { accrueInterest(); IERC20(baseToken).transferFrom(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); } 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); } 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).transferFrom(msg.sender, address(this), amount); userCollateral[msg.sender][asset] += amount; emit SupplyCollateral(msg.sender, msg.sender, asset, amount); } 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); } 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(); uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken); 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 = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset); uint256 assetScale = 10 ** assetConfig.decimals; uint256 collateralValueUSD = (collateralAmount * assetPrice) / assetScale; uint256 discountedValue = (collateralAmount * assetPrice * assetConfig.liquidationFactor) / (assetScale * 1e18); totalCollateralValue += discountedValue; userCollateral[borrower][asset] = 0; collateralReserves[asset] += collateralAmount; emit AbsorbCollateral(absorber, borrower, asset, collateralAmount, collateralValueUSD); } } uint256 baseScale = 10 ** IERC20Metadata(baseToken).decimals(); uint256 collateralInBase = (totalCollateralValue * baseScale) / basePrice; 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); totalSupplyBase += supplyAmount; totalBorrowBase -= repayAmount; uint256 basePaidOut = 0; if (int256(collateralInBase) < -oldBalance) { basePaidOut = uint256(-oldBalance) - collateralInBase; } uint256 valueOfBasePaidOut = (basePaidOut * basePrice) / baseScale; emit AbsorbDebt(absorber, borrower, basePaidOut, valueOfBasePaidOut); } function absorb(address borrower) external override nonReentrant whenNotPaused { accrueInterest(); _absorbInternal(msg.sender, borrower); } 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++; } } } function buyCollateral( address asset, uint256 minAmount, uint256 baseAmount, address recipient ) external override nonReentrant whenNotPaused { if (collateralReserves[asset] == 0) revert InsufficientBalance(); int256 currentReserves = getReserves(); if (currentReserves >= 0 && uint256(currentReserves) >= targetReserves) { revert NotForSale(); } uint256 collateralAmount = quoteCollateral(asset, baseAmount); if (collateralAmount < minAmount) revert InsufficientBalance(); if (collateralAmount > collateralReserves[asset]) revert InsufficientBalance(); IERC20(baseToken).transferFrom(msg.sender, address(this), baseAmount); collateralReserves[asset] -= collateralAmount; IERC20(asset).safeTransfer(recipient, collateralAmount); emit BuyCollateral(msg.sender, asset, baseAmount, collateralAmount); } function quoteCollateral(address asset, uint256 baseAmount) public view override returns (uint256) { AssetConfig memory assetConfig = assetConfigs[asset]; uint256 assetPrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset); uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken); uint256 FACTOR_SCALE = 1e18; uint256 baseScale = 10 ** uint256(IERC20Metadata(baseToken).decimals()); uint256 assetScale = 10 ** uint256(assetConfig.decimals); uint256 discountFactor = (storeFrontPriceFactor * (FACTOR_SCALE - assetConfig.liquidationFactor)) / FACTOR_SCALE; uint256 effectiveAssetPrice = (assetPrice * (FACTOR_SCALE - discountFactor)) / FACTOR_SCALE; if (baseScale == assetScale) { return (baseAmount * basePrice) / effectiveAssetPrice; } else { uint256 adjustedAmount = (baseAmount * assetScale) / baseScale; return (adjustedAmount * basePrice) / effectiveAssetPrice; } } function _isSolvent(address account) internal view returns (bool) { int104 principal = userBasic[account].principal; if (principal >= 0) return true; int256 balance = LendingMath.principalToBalance(principal, borrowIndex); uint256 debt = uint256(-balance); uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken); uint256 baseDecimals = IERC20Metadata(baseToken).decimals(); uint256 debtValue = (debt * basePrice) / (10 ** baseDecimals); uint256 borrowCapacity = _getCollateralValue(account); return borrowCapacity >= debtValue; } 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 = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset); uint256 value = LendingMath.getCollateralValue(amount, price, config.decimals); totalValue += (value * config.borrowCollateralFactor) / 1e18; } } return totalValue; } function getBalance(address account) external view override returns (int256) { int104 principal = userBasic[account].principal; return LendingMath.principalToBalance(principal, supplyIndex); } function supplyBalanceOf(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); uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken); 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 = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset); 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() public view override returns (int256) { uint256 timeElapsed = block.timestamp - lastAccrualTime; (uint256 newSupplyIndex, uint256 newBorrowIndex) = accruedInterestIndices(timeElapsed); uint256 balance = IERC20(baseToken).balanceOf(address(this)); uint256 totalSupply = (uint256(totalSupplyBase) * newSupplyIndex) / 1e18; uint256 totalBorrow = (uint256(totalBorrowBase) * newBorrowIndex) / 1e18; 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 ); return perSecondRate * 31536000; } 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 ); return perSecondRate * 31536000; } function withdrawReserves(address to, uint256 amount) external override onlyOwner nonReentrant { int256 currentReserves = getReserves(); if (currentReserves < 0 || amount > uint256(currentReserves)) { revert InsufficientReserves(); } IERC20(baseToken).safeTransfer(to, amount); emit WithdrawReserves(to, amount); } uint256[50] private __gap; }