// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Math} from "@openzeppelin-contracts-5.6.1/utils/math/Math.sol"; import {Constants} from "./Constants.sol"; /// @title LtvMath /// @notice Pure loan-to-value math. External oracle prices follow the `IAmishOracle` convention — /// value of one *whole* collateral token in *whole* principal tokens, scaled by /// `10 ** priceDecimals` (fixed per adapter, Chainlink-style). {normalizePrice} converts that /// into the internal price used by the rest of the library: principal base units per collateral /// base unit, PRECISION-scaled — folding the price/collateral/principal decimals in one place so the /// valuation functions stay decimal-free. All amounts are token base units; LTV is basis /// points. Rounding is chosen to stay conservative for the lender. library LtvMath { /// @dev The internal normalized price carries 18 decimals of fixed-point precision. Internal to /// this library only — not part of the oracle interface. uint256 internal constant PRECISION = 1e18; /// @dev Largest token/oracle decimal count the valuation math accepts. Real ERC-20s and price feeds /// are <= 18; 30 leaves head-room while keeping every power-of-ten below the uint256 overflow /// cliff — `10^(30+18)` (principal * PRECISION) and `10^(30+30)` (price * collateral) both fit /// with wide margin — so the decimal terms in {normalizePrice} can never overflow. uint8 internal constant MAX_DECIMALS = 30; /// @notice A token or oracle reported more decimals than {MAX_DECIMALS}; unsupported. error LtvMath_DecimalsOutOfRange(); /// @notice Reject a match whose token/oracle decimals fall outside the supported range. Checked /// once at loan creation: a token's decimals and its oracle's decimals are fixed for the /// life of a match, and both are denormalized into the loan, so the hot-path valuation /// (which re-runs {normalizePrice} on every interaction) needs no repeat check. function validateDecimals(uint8 priceDecimals, uint8 collateralDecimals, uint8 principalDecimals) internal pure { if (priceDecimals > MAX_DECIMALS || collateralDecimals > MAX_DECIMALS || principalDecimals > MAX_DECIMALS) { revert LtvMath_DecimalsOutOfRange(); } } /// @notice Convert an adapter price into the internal normalized price (principal base units per /// collateral base unit, PRECISION-scaled), applying the price and both tokens' decimals. /// Rounds down. `priceDecimals`/`collateralDecimals`/`principalDecimals` are token/adapter /// constants small enough that `10 ** d` cannot overflow. function normalizePrice(uint256 price, uint8 priceDecimals, uint8 collateralDecimals, uint8 principalDecimals) internal pure returns (uint256) { // Rebase the whole-token price into principal base units per collateral base unit, then scale // by PRECISION so the fractional ratio survives as an integer: // price / 10^priceDec (whole principal per whole collateral) // * 10^principalDec / 10^collateralDec (whole -> base units on each side) // * PRECISION return Math.mulDiv( price, (10 ** principalDecimals) * PRECISION, (10 ** priceDecimals) * (10 ** collateralDecimals) ); } /// @notice Value of `collateralAmount` (base units) in principal base units, given a normalized /// price. Rounds down, so a borderline position is never valued higher than the feed /// supports. function collateralValue(uint256 collateralAmount, uint256 normalizedPrice) internal pure returns (uint256) { return Math.mulDiv(collateralAmount, normalizedPrice, PRECISION); } /// @notice Collateral amount (base units) worth `value` principal base units at `normalizedPrice`, /// the inverse of {collateralValue}. Rounds down, so the amount seized to cover a given /// value is never rounded up against the borrower. `normalizedPrice` must be non-zero. function valueToCollateral(uint256 value, uint256 normalizedPrice) internal pure returns (uint256) { return Math.mulDiv(value, PRECISION, normalizedPrice, Math.Rounding.Floor); } /// @notice Loan-to-value in bps for `debt` against `collValue` (both in principal base units). /// Rounds up, so a borderline position reads as slightly more leveraged. `collValue` must /// be non-zero (the caller rejects a zero price upstream). function ltvBps(uint256 debt, uint256 collValue) internal pure returns (uint256) { return Math.mulDiv(debt, Constants.BPS, collValue, Math.Rounding.Ceil); } /// @notice Smallest collateral amount (base units) that opens a `debt` position at or below /// `targetLtvBps` at the given normalized price. The exact inverse of {collateralValue}, /// rounded up so the locked collateral never leaves the position above the target. /// `targetLtvBps` and `normalizedPrice` must be non-zero. function collateralForLtv(uint256 debt, uint16 targetLtvBps, uint256 normalizedPrice) internal pure returns (uint256) { uint256 neededValue = Math.mulDiv(debt, Constants.BPS, targetLtvBps, Math.Rounding.Ceil); return Math.mulDiv(neededValue, PRECISION, normalizedPrice, Math.Rounding.Ceil); } }