// SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {IMorpho, IMorphoOracle} from "../MorphoAdapter.sol"; interface IAavePoolL { function getUserAccountData(address user) external view returns (uint256, uint256, uint256, uint256, uint256, uint256); } interface IMorphoFlashL { function position(bytes32 id, address user) external view returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral); function accrueInterest(IMorpho.MarketParams calldata m) external; function market(bytes32 id) external view returns (uint128, uint128, uint128, uint128, uint128, uint128); } /// @title LoanHealth — the LTV and health-check reads of LoanShiftAdapter. /// /// PUBLIC, so it is DELEGATECALLED rather than inlined — the point is to take /// this bytecode OUT of LoanShiftAdapter, which sits over EIP-170. It holds no /// storage. Behaviour is verbatim; `test/LoanShiftAdapter.t.sol` covers the LTV /// ceiling and the "no more levered after an unwind" guard that route here. /// /// The Aave and Morpho interfaces are redeclared locally rather than shared — /// only addresses and `IMorpho.MarketParams` (imported from MorphoAdapter, the /// one type that crosses the boundary) pass between the adapter and here. library LoanHealth { uint256 internal constant BPS = 10_000; /// Measurement noise is sub-bp; 200 bps admitted real leverage ratchets. H-8. uint256 internal constant MAX_LEVERAGE_DRIFT_BPS = 10; /// Dust floor for "empty destination" on owner-migrate. Below this, no-debt /// collateral is ignored so a 1-wei donation cannot DoS the path; real /// lookalike collateral sits far above it. uint256 internal constant DUST_COLLATERAL = 1_000; error NoPositionData(address venue); error MoreLeveredAfterUnwind(uint256 debt, uint256 collateral); /// An unwind must not leave the position MORE levered than it began — the /// aggregate check the flash path cannot make step by step. function requireNoMoreLevered( address pool, address account, uint256 collBefore, uint256 debtBefore ) public view { (uint256 collAfter, uint256 debtAfter,,,,) = IAavePoolL(pool).getUserAccountData(account); if (debtAfter == 0) return; if (collAfter == 0) revert MoreLeveredAfterUnwind(debtAfter, collAfter); uint256 was = debtBefore * collAfter; uint256 now_ = debtAfter * collBefore; if (now_ > was + (was * MAX_LEVERAGE_DRIFT_BPS) / 10_000) { revert MoreLeveredAfterUnwind(debtAfter, collAfter); } } function requireNoMoreLeveredBps(uint256 was, uint256 now_) public pure { if (now_ == 0) return; // Ceil the allowed bump so floored LTV inputs cannot stack with a // truncated `(was * drift) / BPS` to admit ~12 bps of exact rise. uint256 maxNow = was + (was * MAX_LEVERAGE_DRIFT_BPS + BPS - 1) / BPS; if (now_ > maxNow) { revert MoreLeveredAfterUnwind(now_, was); } } /// Same invariant as `requireNoMoreLevered`, on raw debt and collateral /// amounts (oracle cancels within a single Morpho market in one tx). Prefer /// this over floored bps on Morpho partial unwinds. function requireNoMoreLeveredRaw( uint256 debtBefore, uint256 collBefore, uint256 debtAfter, uint256 collAfter ) public pure { if (debtAfter == 0) return; if (collAfter == 0) revert MoreLeveredAfterUnwind(debtAfter, collAfter); uint256 was = debtBefore * collAfter; uint256 now_ = debtAfter * collBefore; if (now_ > was + (was * MAX_LEVERAGE_DRIFT_BPS + BPS - 1) / BPS) { revert MoreLeveredAfterUnwind(debtAfter, collAfter); } } function aaveLtvBps(address pool, address account) public view returns (uint256) { (uint256 collateral, uint256 debt,,,,) = IAavePoolL(pool).getUserAccountData(account); if (collateral == 0) return debt == 0 ? 0 : type(uint256).max; return (debt * BPS + collateral - 1) / collateral; } /// v4 exposes a threshold and a health factor and NOT the amounts, so the /// ratio comes out of those two: HF = collateral x threshold / debt, hence /// debt / collateral = threshold / HF. function aaveV4LtvBps(address spoke, address account) public view returns (uint256) { (bool ok, bytes memory ret) = spoke.staticcall( abi.encodeWithSelector(bytes4(keccak256("getUserAccountData(address)")), account) ); if (!ok || ret.length < 192) revert NoPositionData(spoke); uint256 thresholdWad; uint256 hfWad; assembly { thresholdWad := mload(add(ret, 64)) hfWad := mload(add(ret, 96)) } return v4LtvBps(thresholdWad, hfWad); } /// The v4 ratio itself, ceiled: threshold / HF in bps. INTERNAL, so every /// caller inlines it and nothing links to this library for it. /// /// A spoke with collateral and NO debt reports HF = type(uint256).max, which /// is zero LTV — and which the `+ hf - 1` of the plain ceiling overflowed on, /// so a debt-free v4 position could not withdraw, close, shift or lever. The /// ceiling is taken as quotient plus a remainder bit instead, which cannot. function v4LtvBps(uint256 thresholdWad, uint256 hfWad) internal pure returns (uint256) { if (thresholdWad == 0 || hfWad == type(uint256).max) return 0; if (hfWad == 0) return type(uint256).max; uint256 n = thresholdWad * BPS; return n / hfWad + (n % hfWad == 0 ? 0 : 1); } /// Morpho's own arithmetic: the debt is `borrowShares` converted up against /// this block's totals — which `debtOf` accrues first — and the collateral /// is valued at `collateral * price / 1e36`. function morphoLtvBps(address morpho, IMorpho.MarketParams memory m, address account) public returns (uint256) { uint256 borrowed = debtOf(morpho, m, account); if (borrowed == 0) return 0; (,, uint128 collateral) = IMorphoFlashL(morpho).position(marketId(m), account); if (collateral == 0) return type(uint256).max; uint256 value = (uint256(collateral) * IMorphoOracle(m.oracle).price()) / 1e36; if (value == 0) return type(uint256).max; return (borrowed * BPS + value - 1) / value; } /// The exact debt: `borrowShares` accrued to this block and converted up. function debtOf(address morpho, IMorpho.MarketParams memory m, address account) public returns (uint256) { IMorphoFlashL(morpho).accrueInterest(m); bytes32 id = marketId(m); (, uint128 shares,) = IMorphoFlashL(morpho).position(id, account); return assetsFor(morpho, id, shares); } // ── internal, inlined within this library ─────────────────────────────── function marketId(IMorpho.MarketParams memory m) internal pure returns (bytes32) { return keccak256(abi.encode(m)); } /// SharesMathLib, inlined: VIRTUAL_SHARES = 1e6, VIRTUAL_ASSETS = 1. Rounded /// UP, because Morpho rounds the debt up in its own favour. function assetsFor(address morpho, bytes32 id, uint256 shares) internal view returns (uint256) { (,, uint128 totalAssets, uint128 totalShares,,) = IMorphoFlashL(morpho).market(id); uint256 num = shares * (uint256(totalAssets) + 1); uint256 den = uint256(totalShares) + 1e6; return (num + den - 1) / den; } }