Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
502 lines
23 KiB
Solidity
502 lines
23 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
|
/**
|
|
* @title AereLendingMarket — isolated single-pair money market
|
|
* @notice One deployment per (collateral, debt) pair. Foundation sets
|
|
* immutable risk params at deploy; suppliers earn yield; borrowers
|
|
* pay interest; AereSink receives a configurable cut of interest
|
|
* (NOT principal) on every accrual via `pendingSinkFee`.
|
|
*
|
|
* WAVE 1 PAIRS planned:
|
|
* Pair | LTV | LiqThr | LiqBonus | Borrow fee bps
|
|
* BUIDL → USDC.e | 90% | 92% | 1% | 10
|
|
* USDY → USDC.e | 88% | 91% | 1% | 10
|
|
* OUSG → USDC.e | 88% | 91% | 1% | 10
|
|
* LBTC → USDC.e | 70% | 78% | 5% | 30
|
|
* SolvBTC → USDC.e | 65% | 73% | 6% | 35
|
|
* pumpBTC → USDC.e | 60% | 70% | 7% | 40
|
|
*
|
|
* ACCOUNTING (scaled-principal with global accrual indices):
|
|
* - `debtOf[user]` is the SCALED principal (in ray, 1e27).
|
|
* Real debt = debtOf[user] * borrowIndex / 1e27.
|
|
* - `supplierScaled[user]` is the SCALED supply. Real supply =
|
|
* supplierScaled[user] * supplyIndex / 1e27.
|
|
* - `borrowIndex` grows at `borrowRatePerSecond * dt` per accrue.
|
|
* - `supplyIndex` grows proportionally so suppliers receive
|
|
* (1 - feeBps) of every interest tick. Fee accumulates as
|
|
* `pendingSinkFee` (in DEBT_TOKEN units) and anyone can flush
|
|
* it to AereSink via `flushSinkFee()`.
|
|
*
|
|
* INSOLVENT LIQUIDATIONS:
|
|
* - When the borrower's collateral can't cover the requested pay
|
|
* plus bonus, we PROPORTIONALLY REDUCE `pay` so the liquidator
|
|
* only purchases what they receive. The residual `unpaid`
|
|
* stays on `debtOf[borrower]` and is emitted as `BadDebt`. A
|
|
* future AereInsuranceFund (or socialised write-down) can
|
|
* clear it; the market never silently masks bad debt.
|
|
*
|
|
* SAFETY:
|
|
* - Per-asset immutable risk params (constructor).
|
|
* - Oracle deviation + staleness guarded by AereLendingOracle.
|
|
* - ReentrancyGuard on all mutating fns.
|
|
* - Pause-borrow / pause-deposit toggles for emergency.
|
|
* LIQUIDATIONS NEVER PAUSE.
|
|
* - No admin withdrawal: Foundation cannot rug supplier deposits.
|
|
*
|
|
* @dev Single-market isolation (one contract = one collateral/debt pair)
|
|
* is chosen over a unified Comptroller pattern for clean blast-radius:
|
|
* a SolvBTC oracle compromise can only hurt the SolvBTC market, not
|
|
* BUIDL.
|
|
*/
|
|
contract AereLendingMarket is Ownable, ReentrancyGuard {
|
|
|
|
uint256 internal constant RAY = 1e27;
|
|
|
|
/* ------------------------------- immutable ------------------------------- */
|
|
|
|
IERC20 public immutable COLLATERAL;
|
|
IERC20 public immutable DEBT_TOKEN;
|
|
address public immutable SINK;
|
|
address public immutable ORACLE;
|
|
|
|
uint16 public immutable LTV_BPS;
|
|
uint16 public immutable LIQ_THRESHOLD_BPS;
|
|
uint16 public immutable LIQ_BONUS_BPS;
|
|
uint16 public immutable BORROW_FEE_BPS;
|
|
|
|
uint256 public immutable MARKET_DEBT_CAP;
|
|
uint8 public immutable COLLATERAL_DECIMALS;
|
|
uint8 public immutable DEBT_DECIMALS;
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
/// @notice user → collateral deposited (in COLLATERAL token units)
|
|
mapping(address => uint256) public collateralOf;
|
|
/// @notice user → SCALED debt principal (multiply by borrowIndex/RAY for real)
|
|
mapping(address => uint256) public debtOf;
|
|
/// @notice user → SCALED supplier balance (multiply by supplyIndex/RAY for real)
|
|
mapping(address => uint256) public supplierScaled;
|
|
|
|
/// @notice cumulative indices (ray, 1e27)
|
|
uint256 public borrowIndex = RAY;
|
|
uint256 public supplyIndex = RAY;
|
|
/// @notice last block.timestamp `accrueInterest` ran
|
|
uint256 public lastAccrual;
|
|
/// @notice total scaled debt principal (sum of debtOf)
|
|
uint256 public totalBorrowsScaled;
|
|
/// @notice total scaled supplier principal (sum of supplierScaled)
|
|
uint256 public totalSuppliedScaled;
|
|
|
|
/// @notice DEBT_TOKEN sitting in the contract earmarked for AereSink
|
|
uint256 public pendingSinkFee;
|
|
|
|
/// @notice borrow rate in ray (1e27) per second. Default ~5% APY:
|
|
/// 0.05 * 1e27 / (365 * 86400) ≈ 1.585e18.
|
|
uint256 public borrowRatePerSecond = 1_585_489_599_188_229_300;
|
|
|
|
bool public borrowPaused;
|
|
bool public depositPaused;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event Deposit(address indexed user, uint256 amount);
|
|
event Withdraw(address indexed user, uint256 amount);
|
|
event Supply(address indexed user, uint256 amount, uint256 scaled);
|
|
event Redeem(address indexed user, uint256 amount, uint256 scaled);
|
|
event Borrow(address indexed user, uint256 amount, uint256 scaled);
|
|
event Repay(address indexed user, uint256 amount, uint256 scaled);
|
|
event Liquidated(address indexed liquidator, address indexed borrower, uint256 repaid, uint256 collateralSeized);
|
|
event BadDebt(address indexed borrower, uint256 unpaidDebt);
|
|
event InterestAccrued(uint256 newBorrowIndex, uint256 newSupplyIndex, uint256 interest, uint256 feeToSink);
|
|
event SinkFeeFlushed(uint256 amount);
|
|
event BorrowRateUpdated(uint256 newRatePerSecond);
|
|
event PausedSet(bool borrowPaused, bool depositPaused);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error ZeroAmount();
|
|
error InsufficientCollateral();
|
|
error InsufficientDebtLiquidity();
|
|
error PositionHealthy();
|
|
error PositionUnhealthy();
|
|
error MarketCapReached();
|
|
error TransferFailed();
|
|
error BorrowPausedErr();
|
|
error DepositPausedErr();
|
|
error InsufficientSupplierBalance();
|
|
|
|
/* ----------------------------- constructor ------------------------------ */
|
|
|
|
struct Config {
|
|
address collateral;
|
|
address debtToken;
|
|
address sink;
|
|
address oracle;
|
|
uint16 ltvBps;
|
|
uint16 liqThresholdBps;
|
|
uint16 liqBonusBps;
|
|
uint16 borrowFeeBps;
|
|
uint256 marketDebtCap;
|
|
uint8 collateralDecimals;
|
|
uint8 debtDecimals;
|
|
}
|
|
|
|
constructor(Config memory c) {
|
|
require(c.collateral != address(0) && c.debtToken != address(0), "zero token");
|
|
require(c.sink != address(0) && c.oracle != address(0), "zero infra");
|
|
require(c.ltvBps <= c.liqThresholdBps && c.liqThresholdBps < 10_000, "thresholds");
|
|
require(c.liqBonusBps < 5_000, "bonus huge");
|
|
require(c.borrowFeeBps <= 5_000, "fee huge");
|
|
|
|
COLLATERAL = IERC20(c.collateral);
|
|
DEBT_TOKEN = IERC20(c.debtToken);
|
|
SINK = c.sink;
|
|
ORACLE = c.oracle;
|
|
LTV_BPS = c.ltvBps;
|
|
LIQ_THRESHOLD_BPS = c.liqThresholdBps;
|
|
LIQ_BONUS_BPS = c.liqBonusBps;
|
|
BORROW_FEE_BPS = c.borrowFeeBps;
|
|
MARKET_DEBT_CAP = c.marketDebtCap;
|
|
COLLATERAL_DECIMALS = c.collateralDecimals;
|
|
DEBT_DECIMALS = c.debtDecimals;
|
|
lastAccrual = block.timestamp;
|
|
}
|
|
|
|
/* ----------------------------- interest accrual ------------------------- */
|
|
|
|
/// @notice Grow borrowIndex + supplyIndex over elapsed seconds, accumulate sink fee.
|
|
function accrueInterest() public {
|
|
uint256 dt = block.timestamp - lastAccrual;
|
|
if (dt == 0) return;
|
|
lastAccrual = block.timestamp;
|
|
if (totalBorrowsScaled == 0) return;
|
|
|
|
uint256 borrowsBefore = (totalBorrowsScaled * borrowIndex) / RAY;
|
|
// borrowIndex grows as (1 + rate * dt) — linear approximation safe at 0.5s blocks.
|
|
uint256 indexFactor = (borrowRatePerSecond * dt); // ray
|
|
uint256 borrowsAfter = borrowsBefore + (borrowsBefore * indexFactor) / RAY;
|
|
uint256 interest = borrowsAfter - borrowsBefore;
|
|
if (interest == 0) {
|
|
borrowIndex += (borrowIndex * indexFactor) / RAY;
|
|
emit InterestAccrued(borrowIndex, supplyIndex, 0, 0);
|
|
return;
|
|
}
|
|
borrowIndex += (borrowIndex * indexFactor) / RAY;
|
|
|
|
uint256 fee = (interest * BORROW_FEE_BPS) / 10_000;
|
|
uint256 toSuppliers = interest - fee;
|
|
|
|
// Grow supplyIndex proportionally so suppliers' real balance
|
|
// increases by `toSuppliers` in aggregate.
|
|
uint256 suppliedReal = (totalSuppliedScaled * supplyIndex) / RAY;
|
|
if (suppliedReal > 0 && toSuppliers > 0) {
|
|
// newSupplyIndex / oldSupplyIndex = (suppliedReal + toSuppliers) / suppliedReal
|
|
supplyIndex = (supplyIndex * (suppliedReal + toSuppliers)) / suppliedReal;
|
|
pendingSinkFee += fee;
|
|
} else {
|
|
// ROUND-3 FIX: borrows outstanding with no live supplier-claims
|
|
// (e.g. all suppliers withdrew via socialised path) would silently
|
|
// leak `toSuppliers` into raw contract balance — uncredited. Route
|
|
// 100% of the interest tick to AereSink instead.
|
|
pendingSinkFee += interest;
|
|
}
|
|
|
|
emit InterestAccrued(borrowIndex, supplyIndex, interest, fee);
|
|
}
|
|
|
|
/// @notice Permissionless: push accumulated fee to AereSink.
|
|
function flushSinkFee() external nonReentrant {
|
|
uint256 amount = pendingSinkFee;
|
|
if (amount == 0) return;
|
|
// Reset before external call (CEI).
|
|
pendingSinkFee = 0;
|
|
// Use forceApprove pattern: zero then set, to support USDT-like tokens.
|
|
DEBT_TOKEN.approve(SINK, 0);
|
|
DEBT_TOKEN.approve(SINK, amount);
|
|
(bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", address(DEBT_TOKEN), amount));
|
|
if (!ok) {
|
|
// Restore so a later call can retry.
|
|
pendingSinkFee = amount;
|
|
DEBT_TOKEN.approve(SINK, 0);
|
|
revert TransferFailed();
|
|
}
|
|
emit SinkFeeFlushed(amount);
|
|
}
|
|
|
|
/* ------------------------------ supply side ----------------------------- */
|
|
|
|
function supply(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
accrueInterest();
|
|
if (!DEBT_TOKEN.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
|
|
uint256 scaled = (amount * RAY) / supplyIndex;
|
|
supplierScaled[msg.sender] += scaled;
|
|
totalSuppliedScaled += scaled;
|
|
emit Supply(msg.sender, amount, scaled);
|
|
}
|
|
|
|
function redeem(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
accrueInterest();
|
|
uint256 userReal = (supplierScaled[msg.sender] * supplyIndex) / RAY;
|
|
if (userReal < amount) revert InsufficientSupplierBalance();
|
|
if (_availableLiquidity() < amount + pendingSinkFee) revert InsufficientDebtLiquidity();
|
|
uint256 scaled = (amount * RAY) / supplyIndex;
|
|
// Round-up protection: never let scaled exceed user's holding.
|
|
if (scaled > supplierScaled[msg.sender]) scaled = supplierScaled[msg.sender];
|
|
supplierScaled[msg.sender] -= scaled;
|
|
totalSuppliedScaled -= scaled;
|
|
if (!DEBT_TOKEN.transfer(msg.sender, amount)) revert TransferFailed();
|
|
emit Redeem(msg.sender, amount, scaled);
|
|
}
|
|
|
|
function supplierBalance(address u) external view returns (uint256) {
|
|
// Lazy-view: doesn't accrue. UI should call accrueInterest first for live yield.
|
|
return (supplierScaled[u] * supplyIndex) / RAY;
|
|
}
|
|
|
|
function supplierPrincipal(address u) external view returns (uint256) {
|
|
// Legacy compat — same as scaled balance pre-interest.
|
|
return supplierScaled[u];
|
|
}
|
|
|
|
/* ----------------------------- borrower side ---------------------------- */
|
|
|
|
function depositCollateral(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
if (depositPaused) revert DepositPausedErr();
|
|
accrueInterest();
|
|
if (!COLLATERAL.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
|
|
collateralOf[msg.sender] += amount;
|
|
emit Deposit(msg.sender, amount);
|
|
}
|
|
|
|
function withdrawCollateral(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
accrueInterest();
|
|
if (collateralOf[msg.sender] < amount) revert InsufficientCollateral();
|
|
uint256 newCol = collateralOf[msg.sender] - amount;
|
|
uint256 debtReal = _realDebt(msg.sender);
|
|
if (debtReal > 0 && !_healthyAt(newCol, debtReal)) revert PositionUnhealthy();
|
|
collateralOf[msg.sender] = newCol;
|
|
if (!COLLATERAL.transfer(msg.sender, amount)) revert TransferFailed();
|
|
emit Withdraw(msg.sender, amount);
|
|
}
|
|
|
|
function borrow(uint256 amount) external nonReentrant {
|
|
if (amount == 0) revert ZeroAmount();
|
|
if (borrowPaused) revert BorrowPausedErr();
|
|
accrueInterest();
|
|
uint256 currentTotalReal = (totalBorrowsScaled * borrowIndex) / RAY;
|
|
if (currentTotalReal + amount > MARKET_DEBT_CAP) revert MarketCapReached();
|
|
// Must leave pendingSinkFee untouched for sink claim.
|
|
if (_availableLiquidity() < amount + pendingSinkFee) revert InsufficientDebtLiquidity();
|
|
|
|
uint256 borrowedScaled = (amount * RAY) / borrowIndex;
|
|
// Round-up to avoid undercharging on dust.
|
|
if ((borrowedScaled * borrowIndex) / RAY < amount) borrowedScaled += 1;
|
|
debtOf[msg.sender] += borrowedScaled;
|
|
totalBorrowsScaled += borrowedScaled;
|
|
|
|
uint256 newDebtReal = _realDebt(msg.sender);
|
|
if (!_healthyAt(collateralOf[msg.sender], newDebtReal)) revert PositionUnhealthy();
|
|
if (!DEBT_TOKEN.transfer(msg.sender, amount)) revert TransferFailed();
|
|
emit Borrow(msg.sender, amount, borrowedScaled);
|
|
}
|
|
|
|
function repay(uint256 amount) external nonReentrant {
|
|
_repayInternal(msg.sender, msg.sender, amount);
|
|
}
|
|
|
|
/// @notice Permissionless: anyone (typically AereInsuranceFund or a
|
|
/// charitable donor) can pay down `account`'s debt. msg.sender
|
|
/// provides the tokens; `account`'s debt and totalBorrowsScaled
|
|
/// are reduced. No bonus, no liquidation — just pure repayment
|
|
/// at face value. Useful for clearing BadDebt residuals after
|
|
/// insolvent liquidations.
|
|
function repayOnBehalfOf(address account, uint256 amount) external nonReentrant {
|
|
_repayInternal(msg.sender, account, amount);
|
|
}
|
|
|
|
function _repayInternal(address payer, address account, uint256 amount) internal {
|
|
if (amount == 0) revert ZeroAmount();
|
|
accrueInterest();
|
|
uint256 debtReal = _realDebt(account);
|
|
if (debtReal == 0) return;
|
|
uint256 pay = amount > debtReal ? debtReal : amount;
|
|
if (!DEBT_TOKEN.transferFrom(payer, address(this), pay)) revert TransferFailed();
|
|
|
|
uint256 repaidScaled = (pay * RAY) / borrowIndex;
|
|
if (pay == debtReal) {
|
|
repaidScaled = debtOf[account];
|
|
}
|
|
debtOf[account] -= repaidScaled;
|
|
totalBorrowsScaled -= repaidScaled;
|
|
emit Repay(account, pay, repaidScaled);
|
|
}
|
|
|
|
/* ------------------------------ liquidation ----------------------------- */
|
|
|
|
/// @notice Liquidate `borrower`. Liquidator pays up to `repayAmount` of
|
|
/// the debt and receives proportional collateral + bonus.
|
|
/// Insolvent case (collateral ≤ value-needed): we proportionally
|
|
/// reduce the actual `pay` charged so the liquidator buys only
|
|
/// what they receive. Residual debt stays on the borrower and
|
|
/// is emitted as `BadDebt`.
|
|
function liquidate(address borrower, uint256 repayAmount) external nonReentrant {
|
|
if (repayAmount == 0) revert ZeroAmount();
|
|
accrueInterest();
|
|
|
|
uint256 collateral = collateralOf[borrower];
|
|
uint256 debtReal = _realDebt(borrower);
|
|
if (debtReal == 0) revert PositionHealthy();
|
|
if (_healthyAtLiqThreshold(collateral, debtReal)) revert PositionHealthy();
|
|
|
|
uint256 pay = repayAmount > debtReal ? debtReal : repayAmount;
|
|
|
|
// LIQUIDATIONS NEVER PAUSE: use the non-freezing liquidation price so a
|
|
// paused/deviating/stale feed can never block clearing bad debt.
|
|
uint256 debtPrice = _priceLiq(address(DEBT_TOKEN));
|
|
uint256 colPrice = _priceLiq(address(COLLATERAL));
|
|
|
|
// value in 18-dec USD
|
|
uint256 valueRepaid = _scaleUp(pay, DEBT_DECIMALS) * debtPrice / 1e18;
|
|
uint256 valueToSeize = valueRepaid + (valueRepaid * LIQ_BONUS_BPS) / 10_000;
|
|
uint256 collateralToSeize = _scaleDown(valueToSeize * 1e18 / colPrice, COLLATERAL_DECIMALS);
|
|
|
|
// If we'd seize more than borrower has, clamp seize AND scale pay down
|
|
// proportionally so liquidator only pays for what they receive.
|
|
if (collateralToSeize > collateral) {
|
|
collateralToSeize = collateral;
|
|
uint256 valueOfClamped = _scaleUp(collateral, COLLATERAL_DECIMALS) * colPrice / 1e18;
|
|
// valueOfClamped = valueRepaidReduced * (1 + LIQ_BONUS_BPS/10_000)
|
|
uint256 valueRepaidReduced = (valueOfClamped * 10_000) / (10_000 + LIQ_BONUS_BPS);
|
|
pay = _scaleDown(valueRepaidReduced * 1e18 / debtPrice, DEBT_DECIMALS);
|
|
if (pay > debtReal) pay = debtReal;
|
|
}
|
|
|
|
if (!DEBT_TOKEN.transferFrom(msg.sender, address(this), pay)) revert TransferFailed();
|
|
|
|
// Update borrower state (use scaled units to stay consistent with index).
|
|
uint256 repaidScaled = (pay * RAY) / borrowIndex;
|
|
if (repaidScaled > debtOf[borrower]) repaidScaled = debtOf[borrower];
|
|
debtOf[borrower] -= repaidScaled;
|
|
totalBorrowsScaled -= repaidScaled;
|
|
collateralOf[borrower] = collateral - collateralToSeize;
|
|
|
|
// Emit any residual debt that remains on a now-zero-collateral borrower.
|
|
uint256 remainingCol = collateralOf[borrower];
|
|
uint256 remainingDebt = _realDebt(borrower);
|
|
if (remainingCol == 0 && remainingDebt > 0) {
|
|
emit BadDebt(borrower, remainingDebt);
|
|
}
|
|
|
|
if (!COLLATERAL.transfer(msg.sender, collateralToSeize)) revert TransferFailed();
|
|
emit Liquidated(msg.sender, borrower, pay, collateralToSeize);
|
|
}
|
|
|
|
/* ----------------------------- admin / governance ----------------------- */
|
|
|
|
function setBorrowRate(uint256 newRatePerSecondRay) external onlyOwner {
|
|
// Cap at 1000% APY (10x) in ray/sec — bounds extreme misconfiguration.
|
|
// 10 * 1e27 / (365 * 86400) ≈ 3.17e20.
|
|
require(newRatePerSecondRay <= 317_097_919_837_645_865_000, "rate too high");
|
|
accrueInterest();
|
|
borrowRatePerSecond = newRatePerSecondRay;
|
|
emit BorrowRateUpdated(newRatePerSecondRay);
|
|
}
|
|
|
|
function setPaused(bool _borrowPaused, bool _depositPaused) external onlyOwner {
|
|
borrowPaused = _borrowPaused;
|
|
depositPaused = _depositPaused;
|
|
emit PausedSet(_borrowPaused, _depositPaused);
|
|
}
|
|
|
|
/* ---------------------------------- views ------------------------------- */
|
|
|
|
/// @notice Returns position health as a ratio scaled by 10_000 (1.0 = 10_000).
|
|
/// Above 10_000 = healthy. Below 10_000 = liquidatable.
|
|
function healthFactor(address user) external view returns (uint256) {
|
|
uint256 debt = _realDebt(user);
|
|
if (debt == 0) return type(uint256).max;
|
|
uint256 col = collateralOf[user];
|
|
uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _price(address(COLLATERAL)) / 1e18;
|
|
uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _price(address(DEBT_TOKEN)) / 1e18;
|
|
return (colVal * LIQ_THRESHOLD_BPS) / debtVal;
|
|
}
|
|
|
|
function debtBalance(address user) external view returns (uint256) {
|
|
return _realDebt(user);
|
|
}
|
|
|
|
function totalBorrowsCurrent() external view returns (uint256) {
|
|
return (totalBorrowsScaled * borrowIndex) / RAY;
|
|
}
|
|
|
|
function totalSuppliedCurrent() external view returns (uint256) {
|
|
return (totalSuppliedScaled * supplyIndex) / RAY;
|
|
}
|
|
|
|
/* ------------------------------ internal -------------------------------- */
|
|
|
|
function _realDebt(address user) internal view returns (uint256) {
|
|
if (debtOf[user] == 0) return 0;
|
|
return (debtOf[user] * borrowIndex) / RAY;
|
|
}
|
|
|
|
function _healthyAt(uint256 col, uint256 debt) internal view returns (bool) {
|
|
if (debt == 0) return true;
|
|
uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _price(address(COLLATERAL)) / 1e18;
|
|
uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _price(address(DEBT_TOKEN)) / 1e18;
|
|
return colVal * LTV_BPS / 10_000 >= debtVal;
|
|
}
|
|
|
|
/// @dev Liquidation-eligibility check. Uses the non-freezing liquidation
|
|
/// price so a paused/deviating/stale feed cannot block a liquidation
|
|
/// that the "LIQUIDATIONS NEVER PAUSE" invariant promises must proceed.
|
|
function _healthyAtLiqThreshold(uint256 col, uint256 debt) internal view returns (bool) {
|
|
if (debt == 0) return true;
|
|
uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _priceLiq(address(COLLATERAL)) / 1e18;
|
|
uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _priceLiq(address(DEBT_TOKEN)) / 1e18;
|
|
return colVal * LIQ_THRESHOLD_BPS / 10_000 >= debtVal;
|
|
}
|
|
|
|
function _price(address asset) internal view returns (uint256) {
|
|
return IAereOracleAdapterLending(ORACLE).getPrice(asset);
|
|
}
|
|
|
|
/// @dev Non-freezing price for the liquidation path only (never pauses, never
|
|
/// applies the deviation cap or staleness revert, falls back to the last
|
|
/// good price). Borrow-side checks keep the strict _price/getPrice so the
|
|
/// circuit-breaker still gates NEW borrows. See AereLendingOracle.
|
|
function _priceLiq(address asset) internal view returns (uint256) {
|
|
return IAereOracleAdapterLending(ORACLE).getPriceForLiquidation(asset);
|
|
}
|
|
|
|
function _availableLiquidity() internal view returns (uint256) {
|
|
return DEBT_TOKEN.balanceOf(address(this));
|
|
}
|
|
|
|
function _scaleUp(uint256 amount, uint8 dec) internal pure returns (uint256) {
|
|
if (dec == 18) return amount;
|
|
if (dec < 18) return amount * (10 ** (18 - dec));
|
|
return amount / (10 ** (dec - 18));
|
|
}
|
|
|
|
function _scaleDown(uint256 amount, uint8 dec) internal pure returns (uint256) {
|
|
if (dec == 18) return amount;
|
|
if (dec < 18) return amount / (10 ** (18 - dec));
|
|
return amount * (10 ** (dec - 18));
|
|
}
|
|
}
|
|
|
|
interface IAereOracleAdapterLending {
|
|
function getPrice(address asset) external view returns (uint256);
|
|
function getPriceForLiquidation(address asset) external view returns (uint256);
|
|
}
|