// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; import {Math} from "@openzeppelin-contracts-5.6.1/utils/math/Math.sol"; import {LendingIntent, CollateralEntry, ExecutedTerms} from "../types/DataTypes.sol"; import {Constants} from "./Constants.sol"; /// @title MatchLib /// @notice Pure matching logic. Given a borrow intent and a lend intent (each with its single /// collateral entry), it verifies the pair is compatible, validates the requested fill /// against both sides' remaining capacity, and derives the executed loan terms. Every derived /// term is recomputed here, so the orchestrator trusts nothing from the submitter beyond the /// two signed intents and which side rested on the book (the maker). The collateral amount is /// the one term left to the orchestrator — it is price/mode-dependent sizing, not matching /// (see {deriveTerms} and {proportionalCollateral}). /// @dev The executed rate is the maker's rate. Compatibility guarantees the borrower's proposed /// risk thresholds are at least as protective as the lender's ceilings, so the derived /// thresholds resolve to the borrower's own values; they are still computed as min/min/max to /// be robust and self-documenting. library MatchLib { error MatchLib_NotBorrowIntent(); error MatchLib_NotLendIntent(); error MatchLib_SelfMatch(); error MatchLib_UnsupportedRateType(); error MatchLib_MarketMismatch(); error MatchLib_DurationMismatch(); error MatchLib_RateIncompatible(); error MatchLib_LtvIncompatible(); error MatchLib_MarginCallThresholdIncompatible(); error MatchLib_LiquidationThresholdIncompatible(); error MatchLib_LiquidationBonusIncompatible(); error MatchLib_ThresholdInvariantViolated(); error MatchLib_LiquidationBonusTooHigh(); error MatchLib_ZeroDuration(); error MatchLib_ZeroFill(); error MatchLib_FillExceedsRemaining(); error MatchLib_PartialFillNotAllowed(); error MatchLib_BelowMinFill(); /// @notice Verify compatibility, validate the fill, and derive the executed terms in one call. /// @param b Borrower intent; @param bc its single collateral entry. /// @param l Lender intent; @param lc its single collateral entry. /// @param fillAmount Matched principal amount for this loan. /// @param borrowerFilled Principal already filled against `b` (excluding this fill). /// @param lenderFilled Principal already filled against `l` (excluding this fill). /// @param makerIsLender True if the lender was the resting maker (executed rate = lender's). function verifyAndDeriveTerms( LendingIntent memory b, CollateralEntry memory bc, LendingIntent memory l, CollateralEntry memory lc, uint256 fillAmount, uint256 borrowerFilled, uint256 lenderFilled, bool makerIsLender ) internal pure returns (ExecutedTerms memory) { checkCompatibility(b, bc, l, lc); validateFill(b, l, fillAmount, borrowerFilled, lenderFilled); return deriveTerms(b, bc, l, lc, fillAmount, makerIsLender); } /// @notice Revert unless the two intents and their collateral entries form a compatible match. function checkCompatibility( LendingIntent memory b, CollateralEntry memory bc, LendingIntent memory l, CollateralEntry memory lc ) internal pure { if (b.intentType != Constants.INTENT_TYPE_BORROW) revert MatchLib_NotBorrowIntent(); if (l.intentType != Constants.INTENT_TYPE_LEND) revert MatchLib_NotLendIntent(); if (b.creator == l.creator) revert MatchLib_SelfMatch(); // Same market: collateral (token, chain, oracle, oracle type), principal (token, chain), rate type. if ( bc.token != lc.token || bc.chainId != lc.chainId || bc.oracleAddress != lc.oracleAddress || bc.oracleType != lc.oracleType || bc.rateType != lc.rateType || b.principalToken != l.principalToken || b.principalChainId != l.principalChainId ) { revert MatchLib_MarketMismatch(); } // v1 services fixed-rate loans only (rate types already match via the market check above). if (bc.rateType != Constants.RATE_TYPE_FIXED) revert MatchLib_UnsupportedRateType(); if (b.duration != l.duration) revert MatchLib_DurationMismatch(); // A zero-duration loan matures the moment it opens, turning it into an instant default. if (b.duration == 0) revert MatchLib_ZeroDuration(); // Borrower's terms must be at least as favorable to the lender as the lender requires. if (bc.rate < lc.rate) revert MatchLib_RateIncompatible(); if (bc.ltv > lc.ltv) revert MatchLib_LtvIncompatible(); if (bc.marginCallThreshold > lc.marginCallThreshold) revert MatchLib_MarginCallThresholdIncompatible(); if (bc.liquidationThreshold > lc.liquidationThreshold) revert MatchLib_LiquidationThresholdIncompatible(); if (bc.maxLiquidationBonus < lc.maxLiquidationBonus) revert MatchLib_LiquidationBonusIncompatible(); // The executed thresholds resolve to the borrower's; enforce the per-loan risk ordering and // keep the whole chain below 100% (a liquidation threshold at or beyond 100% would let a // position run insolvent-by-construction before it is ever liquidatable). LTV must be // non-zero: a zero-leverage loan is meaningless, and adaptive collateral sizing divides by it. if (!(0 < bc.ltv && bc.ltv < bc.marginCallThreshold && bc.marginCallThreshold < bc.liquidationThreshold && bc.liquidationThreshold < Constants.BPS)) { revert MatchLib_ThresholdInvariantViolated(); } // The executed bonus cap also resolves to the borrower's. Above 100% a liquidation would claim // collateral worth more than twice the repayment — no solvent auction needs that, and the // partial-liquidation restore math degenerates well before it. if (bc.maxLiquidationBonus > Constants.BPS) revert MatchLib_LiquidationBonusTooHigh(); } /// @notice Revert unless `fillAmount` is valid against both sides' remaining capacity and their /// partial-fill / minimum-fill constraints. A fill that exactly closes a side out is /// always allowed regardless of that side's minimum-fill floor. function validateFill( LendingIntent memory b, LendingIntent memory l, uint256 fillAmount, uint256 borrowerFilled, uint256 lenderFilled ) internal pure { if (fillAmount == 0) revert MatchLib_ZeroFill(); uint256 borrowerRemaining = b.principalAmount - borrowerFilled; uint256 lenderRemaining = l.principalAmount - lenderFilled; if (fillAmount > borrowerRemaining || fillAmount > lenderRemaining) revert MatchLib_FillExceedsRemaining(); if (!b.partialFill && fillAmount != b.principalAmount) revert MatchLib_PartialFillNotAllowed(); if (!l.partialFill && fillAmount != l.principalAmount) revert MatchLib_PartialFillNotAllowed(); if (fillAmount < b.minFillAmount && fillAmount != borrowerRemaining) revert MatchLib_BelowMinFill(); if (fillAmount < l.minFillAmount && fillAmount != lenderRemaining) revert MatchLib_BelowMinFill(); } /// @notice Derive the executed loan terms. Assumes the pair and fill have been validated. /// @dev `collateralAmount` is left unset: it is price- and mode-dependent sizing, not a matching /// term, so the caller populates it (proportional via {proportionalCollateral} for strict /// intents, or oracle-priced adaptive sizing for non-strict ones). function deriveTerms( LendingIntent memory b, CollateralEntry memory bc, LendingIntent memory l, CollateralEntry memory lc, uint256 fillAmount, bool makerIsLender ) internal pure returns (ExecutedTerms memory t) { t.borrower = b.creator; t.lender = l.creator; t.collateralToken = bc.token; t.collateralChainId = bc.chainId; // t.collateralAmount is sized by the caller (see @dev above). t.oracle = bc.oracleAddress; t.oracleType = bc.oracleType; t.maxStaleness = bc.maxStaleness < lc.maxStaleness ? bc.maxStaleness : lc.maxStaleness; t.ltv = bc.ltv; t.marginCallThreshold = _min16(bc.marginCallThreshold, lc.marginCallThreshold); t.liquidationThreshold = _min16(bc.liquidationThreshold, lc.liquidationThreshold); t.maxLiquidationBonus = _max16(bc.maxLiquidationBonus, lc.maxLiquidationBonus); t.rate = makerIsLender ? lc.rate : bc.rate; t.rateType = bc.rateType; t.principalToken = b.principalToken; t.principalChainId = b.principalChainId; t.principalAmount = fillAmount; t.duration = b.duration; // Soulbound if either side requires it. t.apnTransferable = b.apnTransferable && l.apnTransferable; } /// @notice Pro-rata collateral for a fill, rounded up so a partial fill never locks less than its /// proportional share (conservative for the lender). This is the strict-mode sizing; the /// caller applies it after a match. Uses a full-width intermediate so the /// `collateralAmount * fillAmount` product cannot overflow before the division. /// @dev `principalAmount` must be non-zero (guaranteed once a fill has passed `validateFill`). function proportionalCollateral(uint256 collateralAmount, uint256 fillAmount, uint256 principalAmount) internal pure returns (uint256) { return Math.mulDiv(collateralAmount, fillAmount, principalAmount, Math.Rounding.Ceil); } function _min16(uint16 a, uint16 c) private pure returns (uint16) { return a < c ? a : c; } function _max16(uint16 a, uint16 c) private pure returns (uint16) { return a > c ? a : c; } }