// 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 AuctionMath /// @notice Pure math for the unified liquidation auction: the reverse-Dutch bonus curve, the two /// zone distress metrics, and the partial-liquidation repayment cap. All ratios are basis /// points (`Constants.BPS = 10_000 = 100%`); this library is price- and token-agnostic, so /// the collateral/value conversions live in {LtvMath}. Amounts (`debt`, `collValue`, /// `repay`) are principal base units. /// @dev The bonus grows with two independent factors — time since the auction opened and the /// position's distress — combined as the probability complement `1 - (1 - t)(1 - d)` so either /// alone can drive it to the cap and both together accelerate it. Rounding is chosen to favor /// the borrower (a lower bonus, less collateral seized) where a direction must be picked. library AuctionMath { /// @notice Elapsed-time fraction of the auction, in bps, capped at 100%. /// `min(BPS, elapsed * BPS / auctionDuration)`. `auctionDuration` must be non-zero /// (guaranteed by the protocol-parameter bound). function timePct(uint256 elapsed, uint256 auctionDuration) internal pure returns (uint256) { uint256 t = Math.mulDiv(elapsed, Constants.BPS, auctionDuration); return t > Constants.BPS ? Constants.BPS : t; } /// @notice Auction progress in bps from a time fraction and a distress fraction (both bps): /// `progress = BPS - (BPS - timePct)(BPS - distressPct) / BPS`. Inputs are clamped to /// [0, BPS]. At (0.5, 0.5) this is 0.75, not 0.5 — both factors compound. function progress(uint256 timePctBps, uint256 distressPctBps) internal pure returns (uint256) { if (timePctBps > Constants.BPS) timePctBps = Constants.BPS; if (distressPctBps > Constants.BPS) distressPctBps = Constants.BPS; // Round the complement up so progress rounds down — a lower bonus, favoring the borrower, // consistent with the rest of this library. uint256 complement = Math.mulDiv(Constants.BPS - timePctBps, Constants.BPS - distressPctBps, Constants.BPS, Math.Rounding.Ceil); return Constants.BPS - complement; } /// @notice Zone-2 (margin-call) distress in bps: `(currentLTV - MCT) / (LT - MCT)`, clamped to /// [0, BPS]. Zero at the MCT boundary, 1 at the LT boundary. Requires `lt > mct` /// (the per-loan `LTV < MCT < LT` invariant guarantees it). function distressZone2(uint256 currentLtv, uint16 mct, uint16 lt) internal pure returns (uint256) { if (currentLtv <= mct) return 0; uint256 d = Math.mulDiv(currentLtv - mct, Constants.BPS, uint256(lt) - mct); return d > Constants.BPS ? Constants.BPS : d; } /// @notice Zone-3 (liquidation) distress in bps: `(1 - HF) / (1 - HF_floor)` with /// `HF = LT / currentLTV`, clamped to [0, BPS]. Zero as HF crosses 1.0 /// (`currentLTV = LT`), 1 at `HF_floor`. `hfFloor` is a bps ratio in (0, BPS). function distressZone3(uint256 currentLtv, uint16 lt, uint256 hfFloor) internal pure returns (uint256) { // HF as a bps ratio; if the position is not actually past LT, there is no Zone-3 distress. uint256 hf = Math.mulDiv(lt, Constants.BPS, currentLtv); if (hf >= Constants.BPS) return 0; uint256 d = Math.mulDiv(Constants.BPS - hf, Constants.BPS, Constants.BPS - hfFloor); return d > Constants.BPS ? Constants.BPS : d; } /// @notice The liquidation bonus in bps for a given progress: `effBmin + (bMax - effBmin) * progress`, /// where `effBmin = min(bMin, bMax)` so a loan never pays more than its borrower-negotiated /// cap (`bMax = maxLiquidationBonus`) even if the global `bMin` is raised above it. Rounds /// down (borrower-favorable). function bonusBps(uint256 bMin, uint16 bMax, uint256 progressBps) internal pure returns (uint256) { uint256 effBmin = bMin < bMax ? bMin : bMax; return effBmin + Math.mulDiv(uint256(bMax) - effBmin, progressBps, Constants.BPS); } /// @notice The principal-token value the liquidator claims for repaying `repay`: `repay * (1 + bonus)`. /// Rounds down so the seized collateral is never rounded up against the borrower. The caller /// converts this value to collateral base units via {LtvMath-valueToCollateral}. function grossUpValue(uint256 repay, uint256 bonus) internal pure returns (uint256) { return Math.mulDiv(repay, Constants.BPS + bonus, Constants.BPS, Math.Rounding.Floor); } /// @notice Largest repayment (principal base units) that restores a Zone-2 position to /// `targetLtvBps` while paying the liquidator `bonus`: /// /// maxRepay = (debt - targetLtv * collValue) / (1 - targetLtv * (1 + bonus)) /// /// Returns 0 if the position is already at/below the target (nothing to do). If the /// denominator is non-positive the target is mathematically unreachable at this bonus /// (only with an atypically high target LTV); it returns `debt` and the caller caps the /// repay to the debt and available collateral. Rounds down. /// @param debt Current debt (outstanding principal + accrued interest). /// @param collValue Collateral value in principal base units at the current price. /// @param targetLtvBps Target LTV to restore to (`MCT - restoreBuffer`), bps. /// @param bonus Auction bonus, bps. function partialMaxRepay(uint256 debt, uint256 collValue, uint16 targetLtvBps, uint256 bonus) internal pure returns (uint256) { uint256 targetValue = Math.mulDiv(targetLtvBps, collValue, Constants.BPS); // targetLtv * collValue if (debt <= targetValue) return 0; // already at or below target — no liquidation needed uint256 bb = Constants.BPS * Constants.BPS; // BPS^2 uint256 sub = uint256(targetLtvBps) * (Constants.BPS + bonus); // targetLtv * (1 + bonus), BPS^2-scaled if (sub >= bb) return debt; // denominator <= 0: target unreachable, let the caller cap return Math.mulDiv(debt - targetValue, bb, bb - sub); } }