MorphoCredit

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/MorphoCredit.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.22;

import {
    Id,
    MarketParams,
    Position,
    Market,
    BorrowerPremium,
    RepaymentStatus,
    PaymentCycle,
    RepaymentObligation,
    MarkdownState,
    IMorphoCredit
} from "./interfaces/IMorpho.sol";
import {IMarkdownController} from "./interfaces/IMarkdownController.sol";
import {IERC20} from "./interfaces/IERC20.sol";
import {IProtocolConfig, MarketConfig} from "./interfaces/IProtocolConfig.sol";
import {ProtocolConfigLib} from "./libraries/ProtocolConfigLib.sol";
import {ICreditLine} from "./interfaces/ICreditLine.sol";
import {Morpho} from "./Morpho.sol";

import {UtilsLib} from "./libraries/UtilsLib.sol";
import {EventsLib} from "./libraries/EventsLib.sol";
import {ErrorsLib} from "./libraries/ErrorsLib.sol";
import {MathLib} from "./libraries/MathLib.sol";
import {MarketParamsLib} from "./libraries/MarketParamsLib.sol";
import {SharesMathLib} from "./libraries/SharesMathLib.sol";
import {SafeTransferLib} from "./libraries/SafeTransferLib.sol";

/// @title Morpho Credit
/// @author 3Jane
/// @custom:contact support@3jane.xyz
/// @notice The Morpho Credit contract extends Morpho with credit-based lending and per-borrower risk premiums.
/// @dev This contract implements a three-tier interest accrual system:
///
/// **Interest Rate Components:**
/// 1. Base Rate: Market-wide rate from the IRM, applied to all borrowers
/// 2. Premium Rate: Per-borrower risk premium based on creditworthiness
/// 3. Penalty Rate: Additional rate when borrower is delinquent (past grace period)
///
/// **Accrual Process Flow:**
/// ```
/// Market Interest (continuous) → Base Rate Growth
///                                      ↓
/// Borrower Premium Accrual → Premium on top of base
///                                      ↓
/// Delinquency Check → If delinquent, add penalty rate
/// ```
/// - Using ending balance for penalty calculations
contract MorphoCredit is Morpho, IMorphoCredit {
    using UtilsLib for uint256;
    using MarketParamsLib for MarketParams;
    using SharesMathLib for uint256;
    using MathLib for uint256;
    using SafeTransferLib for IERC20;

    /* STATE VARIABLES */

    /// @inheritdoc IMorphoCredit
    address public helper;

    /// @notice Immutable protocol configuration contract
    /// @custom:oz-upgrades-unsafe-allow state-variable-immutable
    address public immutable protocolConfig;

    /// @inheritdoc IMorphoCredit
    address public usd3;

    /// @inheritdoc IMorphoCredit
    mapping(Id => mapping(address => BorrowerPremium)) public borrowerPremium;

    /// @notice Payment cycles for each market
    mapping(Id => PaymentCycle[]) public paymentCycle;

    /// @notice Repayment obligations for each borrower in each market
    mapping(Id => mapping(address => RepaymentObligation)) public repaymentObligation;

    /// @notice Markdown state for tracking defaulted debt value reduction
    mapping(Id => mapping(address => MarkdownState)) public markdownState;

    /// @dev Storage gap for future upgrades (14 slots).
    uint256[14] private __gap;

    /* CONSTANTS */

    uint256 internal constant MIN_PREMIUM_THRESHOLD = 1;

    /// @notice Maximum elapsed time for premium accrual (365 days)
    uint256 internal constant MAX_ELAPSED_TIME = 365 days;

    /// @notice Maximum basis points (100%)
    uint256 internal constant MAX_BPS = 10000;

    /* INITIALIZER */

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor(address _protocolConfig) {
        if (_protocolConfig == address(0)) revert ErrorsLib.ZeroAddress();
        protocolConfig = _protocolConfig;
        _disableInitializers();
    }

    /// @dev Initializes the MorphoCredit contract.
    /// @param newOwner The initial owner of the contract.
    function initialize(address newOwner) external initializer {
        __Morpho_init(newOwner);
    }

    /* ADMIN FUNCTIONS */

    /// @inheritdoc IMorphoCredit
    function setHelper(address newHelper) external onlyOwner {
        helper = newHelper;
    }

    /// @inheritdoc IMorphoCredit
    function setUsd3(address newUsd3) external onlyOwner {
        usd3 = newUsd3;
    }

    /* EXTERNAL FUNCTIONS - PREMIUM MANAGEMENT */

    /// @inheritdoc IMorphoCredit
    function accruePremiumsForBorrowers(Id id, address[] calldata borrowers) external {
        if (market[id].lastUpdate == 0) revert ErrorsLib.MarketNotCreated();
        MarketParams memory marketParams = idToMarketParams[id];
        _accrueInterest(marketParams, id);
        for (uint256 i = 0; i < borrowers.length; i++) {
            _accrueBorrowerPremiumAndUpdate(id, borrowers[i]);
        }
    }

    /// @dev Internal helper to accrue premium and update borrower state
    function _accrueBorrowerPremiumAndUpdate(Id id, address borrower) internal {
        _accrueBorrowerPremium(id, borrower);
        _updateBorrowerMarkdown(id, borrower);
        _snapshotBorrowerPosition(id, borrower);
    }

    /* INTERNAL FUNCTIONS - PREMIUM CALCULATIONS */

    /// @dev Calculates the premium amount to be added based on observed base rate growth
    /// @param borrowAssetsAtLastAccrual The borrower's assets at last premium accrual
    /// @param borrowAssetsCurrent The borrower's current assets (including base interest)
    /// @param premiumRate The borrower's premium rate per second (scaled by WAD)
    /// @param elapsed Time elapsed since last accrual
    /// @return premiumAmount The premium amount to add
    function _calculateBorrowerPremiumAmount(
        uint256 borrowAssetsAtLastAccrual,
        uint256 borrowAssetsCurrent,
        uint256 premiumRate,
        uint256 elapsed
    ) internal pure returns (uint256 premiumAmount) {
        // Prevent division by zero
        if (borrowAssetsAtLastAccrual == 0 || elapsed == 0) return 0;

        // Calculate the actual base growth
        uint256 baseGrowthActual;
        uint256 baseRatePerSecond;
        if (borrowAssetsCurrent > borrowAssetsAtLastAccrual) {
            baseGrowthActual = borrowAssetsCurrent - borrowAssetsAtLastAccrual;
            baseRatePerSecond = borrowAssetsCurrent.wDivUp(borrowAssetsAtLastAccrual).wInverseTaylorCompounded(elapsed);
        }

        // Combine base rate with premium rate (both per-second)
        uint256 combinedRate = baseRatePerSecond + premiumRate;

        // Calculate compound growth using wTaylorCompounded
        uint256 totalGrowth = combinedRate.wTaylorCompounded(elapsed);
        uint256 totalGrowthAmount = borrowAssetsAtLastAccrual.wMulDown(totalGrowth);

        // Premium amount is the difference between total growth and actual base growth
        premiumAmount = totalGrowthAmount > baseGrowthActual ? totalGrowthAmount - baseGrowthActual : 0;
    }

    /// @dev Calculate ongoing premium and penalty rate if already past grace period
    /// @return premiumAmount The calculated premium amount (including penalty if applicable)
    function _calculateOngoingPremiumAndPenalty(
        Id id,
        address borrower,
        BorrowerPremium memory premium,
        RepaymentStatus status,
        uint256 borrowAssetsCurrent
    ) internal view returns (uint256 premiumAmount) {
        uint256 elapsed = block.timestamp - premium.lastAccrualTime;
        if (elapsed == 0) return 0;

        if (elapsed > MAX_ELAPSED_TIME) {
            elapsed = MAX_ELAPSED_TIME;
        }

        uint256 totalPremiumRate = premium.rate;

        // Add penalty rate if already in penalty period (after grace period)
        if (status != RepaymentStatus.Current && status != RepaymentStatus.GracePeriod) {
            RepaymentObligation memory obligation = repaymentObligation[id][borrower];
            uint256 cycleEndDate = paymentCycle[id][obligation.paymentCycleId].endDate;
            MarketConfig memory terms = IProtocolConfig(protocolConfig).getMarketConfig();

            if (premium.lastAccrualTime > cycleEndDate + terms.gracePeriod) {
                totalPremiumRate += terms.irp;
            }
        }

        premiumAmount = _calculateBorrowerPremiumAmount(
            premium.borrowAssetsAtLastAccrual, borrowAssetsCurrent, totalPremiumRate, elapsed
        );
    }

    /// @dev Calculate initial penalty when first transitioning into penalty period
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @param status Current repayment status
    /// @param borrowAssetsCurrent Current borrow assets after base accrual
    /// @param basePremiumAmount Premium amount already calculated
    /// @return penaltyAmount The calculated penalty amount
    /// @dev Penalty calculation logic:
    /// 1. Only applies if status is Delinquent or Default
    /// 2. Only for first accrual after grace period ends (lastAccrualTime <= cycleEndDate + gracePeriod)
    /// 3. Uses ending balance from obligation as the principal
    /// 4. Calculates penalty from cycle end date to now
    /// 5. Adds basePremiumAmount to current assets for accurate compounding
    function _calculateInitialPenalty(
        Id id,
        address borrower,
        RepaymentStatus status,
        uint256 borrowAssetsCurrent,
        uint256 basePremiumAmount
    ) internal view returns (uint256 penaltyAmount) {
        if (status != RepaymentStatus.Delinquent && status != RepaymentStatus.Default) {
            return 0;
        }

        BorrowerPremium memory premium = borrowerPremium[id][borrower];
        RepaymentObligation memory obligation = repaymentObligation[id][borrower];
        uint256 cycleEndDate = paymentCycle[id][obligation.paymentCycleId].endDate;
        MarketConfig memory terms = IProtocolConfig(protocolConfig).getMarketConfig();

        if (premium.lastAccrualTime > cycleEndDate + terms.gracePeriod) {
            return 0; // Already handled in premium calculation
        }

        uint256 elapsed = block.timestamp - cycleEndDate;
        if (elapsed > MAX_ELAPSED_TIME) {
            elapsed = MAX_ELAPSED_TIME;
        }
        penaltyAmount = _calculateBorrowerPremiumAmount(
            obligation.endingBalance, borrowAssetsCurrent + basePremiumAmount, terms.irp, elapsed
        );
    }

    /// @dev Update position and market with premium shares
    function _updatePositionWithPremium(Id id, address borrower, uint256 premiumAmount) internal {
        Market memory targetMarket = market[id];

        uint256 premiumShares = premiumAmount.toSharesUp(targetMarket.totalBorrowAssets, targetMarket.totalBorrowShares);

        // Update borrower position
        position[id][borrower].borrowShares += premiumShares.toUint128();

        // Update market totals
        targetMarket.totalBorrowShares += premiumShares.toUint128();
        targetMarket.totalBorrowAssets += premiumAmount.toUint128();
        targetMarket.totalSupplyAssets += premiumAmount.toUint128();

        // Handle fees
        uint256 feeAmount;
        if (targetMarket.fee != 0) {
            feeAmount = premiumAmount.wMulDown(targetMarket.fee);
            uint256 feeShares =
                feeAmount.toSharesDown(targetMarket.totalSupplyAssets - feeAmount, targetMarket.totalSupplyShares);
            position[id][feeRecipient].supplyShares += feeShares;
            targetMarket.totalSupplyShares += feeShares.toUint128();
        }

        // Write back to storage
        market[id] = targetMarket;

        emit EventsLib.PremiumAccrued(id, borrower, premiumAmount, feeAmount);
    }

    /// @notice Accrue premium for a specific borrower
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @dev Core accrual function that orchestrates the premium and penalty calculation process:
    /// 1. Check repayment status
    /// 2. Calculate premium based on elapsed time and rates
    /// 3. Calculate penalty if borrower is delinquent/default (not during grace period)
    /// 4. Apply combined premium+penalty as new borrow shares
    /// 5. Update timestamp to prevent double accrual
    /// @dev MUST be called after _accrueInterest to ensure base rate is current
    function _accrueBorrowerPremium(Id id, address borrower) internal {
        (RepaymentStatus status,) = getRepaymentStatus(id, borrower);

        BorrowerPremium memory premium = borrowerPremium[id][borrower];
        if (premium.rate == 0 && status == RepaymentStatus.Current) return;

        if (position[id][borrower].borrowShares == 0) return;

        // Calculate current borrow assets
        Market memory targetMarket = market[id];
        uint256 borrowAssetsCurrent = uint256(position[id][borrower].borrowShares)
            .toAssetsUp(targetMarket.totalBorrowAssets, targetMarket.totalBorrowShares);

        // Calculate premium and penalty accruals
        uint256 premiumAmount = _calculateOngoingPremiumAndPenalty(id, borrower, premium, status, borrowAssetsCurrent);

        // Calculate penalty if needed (handles first penalty accrual)
        uint256 penaltyAmount = _calculateInitialPenalty(id, borrower, status, borrowAssetsCurrent, premiumAmount);

        uint256 totalPremium = premiumAmount + penaltyAmount;

        // Skip if below threshold
        if (totalPremium < MIN_PREMIUM_THRESHOLD) {
            return;
        }

        // Apply the premium
        _updatePositionWithPremium(id, borrower, totalPremium);

        // Update timestamp
        borrowerPremium[id][borrower].lastAccrualTime = uint128(block.timestamp);
    }

    /// @notice Snapshot borrower's position for premium tracking
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @dev Snapshots are critical for accurate premium calculation:
    /// - Captures the current borrow amount after all accruals
    /// - Updates borrowAssetsAtLastAccrual to this new value
    /// - Ensures next premium calculation starts from correct base
    /// - Only updates if borrower has a premium rate set
    /// @dev Called after every borrow, repay, or liquidation to maintain accuracy
    function _snapshotBorrowerPosition(Id id, address borrower) internal {
        BorrowerPremium memory premium = borrowerPremium[id][borrower];

        Market memory targetMarket = market[id];

        uint256 currentBorrowAssets = uint256(position[id][borrower].borrowShares)
            .toAssetsUp(targetMarket.totalBorrowAssets, targetMarket.totalBorrowShares);

        // Update timestamp if:
        // - Not initialized (safety check), OR
        // - This is the first actual borrow (transition from 0 debt to positive debt)
        if (premium.lastAccrualTime == 0 || (premium.borrowAssetsAtLastAccrual == 0 && currentBorrowAssets > 0)) {
            premium.lastAccrualTime = uint128(block.timestamp);
        }

        // Update borrow amount snapshot
        premium.borrowAssetsAtLastAccrual = currentBorrowAssets.toUint128();

        // Write back to storage
        borrowerPremium[id][borrower] = premium;
    }

    /* MODIFIERS */

    /// @notice Modifier to restrict access to the market's CreditLine contract
    modifier onlyCreditLine(Id id) {
        requireCreditLine(id);
        _;
    }

    function requireCreditLine(Id id) internal {
        if (market[id].lastUpdate == 0) revert ErrorsLib.MarketNotCreated();
        if (msg.sender != idToMarketParams[id].creditLine) revert ErrorsLib.NotCreditLine();
    }

    /* EXTERNAL FUNCTIONS - CREDIT LINE MANAGEMENT */

    /// @inheritdoc IMorphoCredit
    function setCreditLine(Id id, address borrower, uint256 credit, uint128 premiumRate) external onlyCreditLine(id) {
        if (borrower == address(0)) revert ErrorsLib.ZeroAddress();

        position[id][borrower].collateral = credit.toUint128();

        emit EventsLib.SetCreditLine(id, borrower, credit);

        _setBorrowerPremiumRate(id, borrower, premiumRate);
    }

    /// @notice Set or update a borrower's premium rate
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @param newRate New premium rate per second in WAD (e.g., 0.1e18 / 365 days for 10% APR)
    function _setBorrowerPremiumRate(Id id, address borrower, uint128 newRate) internal {
        // Accrue base interest first to ensure premium calculations are accurate
        MarketParams memory marketParams = idToMarketParams[id];
        _accrueInterest(marketParams, id);

        // If there's an existing position with borrow shares, accrue premium
        uint256 borrowShares = uint256(position[id][borrower].borrowShares);
        if (borrowShares > 0) {
            _accrueBorrowerPremium(id, borrower);
        }

        // Load premium from storage
        BorrowerPremium memory premium = borrowerPremium[id][borrower];
        uint128 oldRate = premium.rate;

        // Set the new rate
        premium.rate = newRate;
        borrowerPremium[id][borrower] = premium;

        // Take snapshot after setting the new rate if there are borrow shares
        if (borrowShares > 0 && newRate > 0) {
            _snapshotBorrowerPosition(id, borrower);
        }

        emit EventsLib.BorrowerPremiumRateSet(id, borrower, oldRate, newRate);
    }

    /* EXTERNAL FUNCTIONS - REPAYMENT MANAGEMENT */

    /// @notice Close a payment cycle and create it on-chain retroactively
    /// @param id Market ID
    /// @param endDate Cycle end date
    /// @param borrowers Array of borrower addresses
    /// @param repaymentBps Array of repayment basis points (e.g., 500 = 5%)
    /// @param endingBalances Array of ending balances for penalty calculations
    /// @dev The ending balance is crucial for penalty calculations - it represents
    /// the borrower's debt at cycle end and is used to calculate penalty interest
    /// from that point forward, ensuring path independence
    function closeCycleAndPostObligations(
        Id id,
        uint256 endDate,
        address[] calldata borrowers,
        uint256[] calldata repaymentBps,
        uint256[] calldata endingBalances
    ) external onlyCreditLine(id) {
        if (borrowers.length != repaymentBps.length || repaymentBps.length != endingBalances.length) {
            revert ErrorsLib.InconsistentInput();
        }
        if (endDate > block.timestamp) revert ErrorsLib.CannotCloseFutureCycle();

        uint256 cycleLength = paymentCycle[id].length;
        uint256 startDate;

        if (cycleLength > 0) {
            PaymentCycle storage prevCycle = paymentCycle[id][cycleLength - 1];
            startDate = prevCycle.endDate;

            uint256 cycleDuration = IProtocolConfig(protocolConfig).getCycleDuration();

            if (cycleDuration > 0 && endDate < startDate + cycleDuration) {
                revert ErrorsLib.InvalidCycleDuration();
            }
        }

        // Create the payment cycle record
        paymentCycle[id].push(PaymentCycle({endDate: endDate}));

        uint256 cycleId = paymentCycle[id].length - 1;

        // Post obligations for this cycle
        for (uint256 i = 0; i < borrowers.length; i++) {
            _postRepaymentObligation(id, borrowers[i], repaymentBps[i], cycleId, endingBalances[i]);
        }

        emit EventsLib.PaymentCycleCreated(id, cycleId, startDate, endDate);
    }

    /// @notice Add more obligations to the most recently closed cycle
    /// @param id Market ID
    /// @param borrowers Array of borrower addresses
    /// @param repaymentBps Array of repayment basis points (e.g., 500 = 5%)
    /// @param endingBalances Array of ending balances
    function addObligationsToLatestCycle(
        Id id,
        address[] calldata borrowers,
        uint256[] calldata repaymentBps,
        uint256[] calldata endingBalances
    ) external onlyCreditLine(id) {
        if (borrowers.length != repaymentBps.length || repaymentBps.length != endingBalances.length) {
            revert ErrorsLib.InconsistentInput();
        }
        if (paymentCycle[id].length == 0) revert ErrorsLib.NoCyclesExist();

        uint256 latestCycleId = paymentCycle[id].length - 1;

        for (uint256 i = 0; i < borrowers.length; i++) {
            _postRepaymentObligation(id, borrowers[i], repaymentBps[i], latestCycleId, endingBalances[i]);
        }
    }

    /// @notice Internal function to post individual obligation
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @param repaymentBps Repayment percentage in basis points (e.g., 500 = 5%)
    /// @param cycleId Payment cycle ID
    /// @param endingBalance Balance at cycle end for penalty calculations
    function _postRepaymentObligation(
        Id id,
        address borrower,
        uint256 repaymentBps,
        uint256 cycleId,
        uint256 endingBalance
    ) internal {
        if (repaymentBps > MAX_BPS) revert ErrorsLib.RepaymentExceedsHundredPercent();

        RepaymentObligation memory obligation = repaymentObligation[id][borrower];

        // Calculate actual amount from basis points
        uint256 amount = endingBalance * repaymentBps / MAX_BPS;

        // Only set cycleId and endingBalance for new obligations
        if (obligation.amountDue == 0) {
            obligation.paymentCycleId = uint128(cycleId);
            obligation.endingBalance = endingBalance.toUint128();
        }

        // Update amount due
        obligation.amountDue = uint128(amount);

        repaymentObligation[id][borrower] = obligation;

        // Emit input parameters to reflect poster's intent
        emit EventsLib.RepaymentObligationPosted(id, borrower, amount, cycleId, endingBalance);
    }

    /// @notice Get repayment status for a borrower
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return _status The borrower's current repayment status
    /// @return _statusStartTime The timestamp when the current status began
    function getRepaymentStatus(Id id, address borrower)
        internal
        view
        returns (RepaymentStatus _status, uint256 _statusStartTime)
    {
        RepaymentObligation memory obligation = repaymentObligation[id][borrower];

        if (obligation.amountDue == 0) return (RepaymentStatus.Current, 0);

        // Validate cycleId is within bounds
        if (obligation.paymentCycleId >= paymentCycle[id].length) revert ErrorsLib.InvalidCycleId();

        _statusStartTime = paymentCycle[id][obligation.paymentCycleId].endDate;

        MarketConfig memory terms = IProtocolConfig(protocolConfig).getMarketConfig();

        if (block.timestamp <= _statusStartTime + terms.gracePeriod) {
            return (RepaymentStatus.GracePeriod, _statusStartTime);
        }
        _statusStartTime += terms.gracePeriod;
        if (block.timestamp < _statusStartTime + terms.delinquencyPeriod) {
            return (RepaymentStatus.Delinquent, _statusStartTime);
        }

        return (RepaymentStatus.Default, _statusStartTime + terms.delinquencyPeriod);
    }

    /* INTERNAL FUNCTIONS - HOOK IMPLEMENTATIONS */

    /// @inheritdoc Morpho
    function _beforeSupply(MarketParams memory, Id id, address onBehalf, uint256, uint256, bytes calldata)
        internal
        virtual
        override
    {
        if (msg.sender != usd3) revert ErrorsLib.NotUsd3();
        if (IProtocolConfig(protocolConfig).getIsPaused() > 0) revert ErrorsLib.Paused();
    }

    /// @inheritdoc Morpho
    function _beforeWithdraw(MarketParams memory, Id id, address onBehalf, uint256, uint256) internal virtual override {
        // Allow USD3 to withdraw on behalf of anyone
        if (msg.sender == usd3) return;

        // Allow fee recipient to withdraw their own fees only
        if (msg.sender == feeRecipient && onBehalf == feeRecipient) return;

        // Reject all other withdrawals
        revert ErrorsLib.NotUsd3();
    }

    /// @inheritdoc Morpho
    function _beforeBorrow(MarketParams memory, Id id, address onBehalf, uint256 assets, uint256)
        internal
        virtual
        override
    {
        if (msg.sender != helper) revert ErrorsLib.NotHelper();
        if (IProtocolConfig(protocolConfig).getIsPaused() > 0) revert ErrorsLib.Paused();
        if (_isMarketFrozen(id)) revert ErrorsLib.MarketFrozen();

        // Check if borrower can borrow
        (RepaymentStatus status,) = getRepaymentStatus(id, onBehalf);
        if (status != RepaymentStatus.Current) revert ErrorsLib.OutstandingRepayment();
        _accrueBorrowerPremium(id, onBehalf);
        // No need to update markdown - borrower must be Current to borrow, so markdown is always 0

        // Check debt cap
        uint256 debtCap = IProtocolConfig(protocolConfig).config(ProtocolConfigLib.DEBT_CAP);
        if (debtCap > 0 && market[id].totalBorrowAssets + assets > debtCap) {
            revert ErrorsLib.DebtCapExceeded();
        }

        // Check minimum borrow requirement
        uint256 minBorrow = IProtocolConfig(protocolConfig).getMarketConfig().minBorrow;
        if (minBorrow > 0) {
            Market memory m = market[id];
            uint256 currentDebt =
                uint256(position[id][onBehalf].borrowShares).toAssetsUp(m.totalBorrowAssets, m.totalBorrowShares);
            uint256 newDebt = currentDebt + assets;
            if (newDebt < minBorrow) revert ErrorsLib.BelowMinimumBorrow();
        }
    }

    /// @inheritdoc Morpho
    /// @dev Accrues premium before tracking payment. During grace period, only base + premium
    /// accrue (no penalty), allowing borrowers to clear obligations without penalty.
    /// During delinquent/default, penalty also accrues before payment is tracked.
    function _beforeRepay(MarketParams memory, Id id, address onBehalf, uint256 assets, uint256)
        internal
        virtual
        override
    {
        if (_isMarketFrozen(id)) revert ErrorsLib.MarketFrozen();

        // Accrue premium (including penalty if past grace period)
        _accrueBorrowerPremium(id, onBehalf);
        _updateBorrowerMarkdown(id, onBehalf); // TODO: decide whether to remove

        // Check minimum borrow requirement for remaining debt
        uint256 minBorrow = IProtocolConfig(protocolConfig).getMarketConfig().minBorrow;
        if (minBorrow > 0) {
            Market memory m = market[id];
            uint256 currentDebt =
                uint256(position[id][onBehalf].borrowShares).toAssetsUp(m.totalBorrowAssets, m.totalBorrowShares);
            if (currentDebt > assets) {
                uint256 remainingDebt = currentDebt - assets;
                if (remainingDebt < minBorrow) revert ErrorsLib.BelowMinimumBorrow();
            }
        }

        // Track payment against obligation
        _trackObligationPayment(id, onBehalf, assets);
    }

    /// @inheritdoc Morpho
    function _afterBorrow(MarketParams memory, Id id, address onBehalf) internal virtual override {
        _snapshotBorrowerPosition(id, onBehalf);
    }

    /// @inheritdoc Morpho
    function _afterRepay(MarketParams memory, Id id, address onBehalf, uint256) internal virtual override {
        _snapshotBorrowerPosition(id, onBehalf);
        _updateBorrowerMarkdown(id, onBehalf);
    }

    /// @dev Track obligation payment and update state
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @param payment Payment amount being made
    /// @dev Enforces minimum payment requirement - must pay full obligation
    /// This prevents partial payments that would leave borrowers in limbo
    function _trackObligationPayment(Id id, address borrower, uint256 payment) internal {
        uint256 amountDue = repaymentObligation[id][borrower].amountDue;

        if (amountDue == 0) return;

        if (payment < amountDue) revert ErrorsLib.MustPayFullObligation();

        // Clear the obligation
        repaymentObligation[id][borrower].amountDue = 0;

        emit EventsLib.RepaymentTracked(id, borrower, payment, 0);
    }

    /* INTERNAL FUNCTIONS - HEALTH CHECK OVERRIDES */

    /// @dev Override health check for credit-based lending (without price)
    /// @param marketParams The market parameters
    /// @param id The market id
    /// @param borrower The borrower address
    /// @return healthy Whether the position is healthy
    function _isHealthy(MarketParams memory marketParams, Id id, address borrower)
        internal
        view
        override
        returns (bool)
    {
        // For credit-based lending, price is irrelevant
        return _isHealthy(marketParams, id, borrower, 0);
    }

    /// @dev Override health check for credit-based lending (with price)
    /// @param marketParams The market parameters
    /// @param id The market id
    /// @param borrower The borrower address
    /// @param collateralPrice The collateral price (unused in credit model)
    /// @return healthy Whether the position is healthy
    function _isHealthy(MarketParams memory marketParams, Id id, address borrower, uint256 collateralPrice)
        internal
        view
        override
        returns (bool)
    {
        Position memory position = position[id][borrower];

        // Early return if no borrow position
        if (position.borrowShares == 0) return true;

        Market memory market = market[id];
        uint256 borrowed = uint256(position.borrowShares).toAssetsUp(market.totalBorrowAssets, market.totalBorrowShares);
        uint256 creditLimit = position.collateral;

        return creditLimit >= borrowed;
    }

    /* MARKDOWN FUNCTIONS */

    /// @notice Update a borrower's markdown state and market total
    /// @param id Market ID
    /// @param borrower Borrower address
    function _updateBorrowerMarkdown(Id id, address borrower) internal {
        address manager = ICreditLine(idToMarketParams[id].creditLine).mm();
        if (manager == address(0)) return; // No markdown manager set

        uint256 lastMarkdown = markdownState[id][borrower].lastCalculatedMarkdown;
        (RepaymentStatus status, uint256 statusStartTime) = getRepaymentStatus(id, borrower);

        // Check if in default and emit status change events
        bool isInDefault = status == RepaymentStatus.Default && statusStartTime > 0;
        bool wasInDefault = lastMarkdown > 0;

        if (isInDefault && !wasInDefault) {
            IMarkdownController(manager).resetBorrowerState(borrower);
            emit EventsLib.DefaultStarted(id, borrower, statusStartTime);
        } else if (!isInDefault && wasInDefault) {
            emit EventsLib.DefaultCleared(id, borrower);
        }

        // Calculate new markdown
        uint256 newMarkdown = 0;
        if (isInDefault) {
            uint256 timeInDefault = block.timestamp > statusStartTime ? block.timestamp - statusStartTime : 0;
            uint256 borrowerAssets = _getBorrowerAssets(id, borrower);

            newMarkdown = IMarkdownController(manager).calculateMarkdown(borrower, borrowerAssets, timeInDefault);

            // Slash JANE proportionally to markdown
            IMarkdownController(manager).slashJaneProportional(borrower, timeInDefault);

            // Cap markdown at the borrower's actual outstanding debt
            // since markdown represents the write-down of the loan value
            if (newMarkdown > borrowerAssets) {
                newMarkdown = borrowerAssets;
            }
        }

        if (newMarkdown != lastMarkdown) {
            // Update borrower state
            markdownState[id][borrower].lastCalculatedMarkdown = uint128(newMarkdown);

            // Update market totals - use a separate function to avoid stack issues
            _updateMarketMarkdown(id, int256(newMarkdown) - int256(lastMarkdown));

            emit EventsLib.BorrowerMarkdownUpdated(id, borrower, lastMarkdown, newMarkdown);
        }
    }

    /// @notice Update market totals for markdown changes
    /// @param id Market ID
    /// @param markdownDelta Change in markdown (positive = increase, negative = decrease)
    function _updateMarketMarkdown(Id id, int256 markdownDelta) internal {
        if (markdownDelta == 0) return;

        Market memory m = market[id];

        if (markdownDelta > 0) {
            // Markdown increasing (borrower deeper in default)
            uint256 increase = uint256(markdownDelta);

            // Cap at available supply to avoid underflow
            if (increase > m.totalSupplyAssets) {
                increase = m.totalSupplyAssets;
            }

            // Apply the reduction to supply and record what was marked down
            m.totalSupplyAssets = (m.totalSupplyAssets - increase).toUint128();
            m.totalMarkdownAmount = (m.totalMarkdownAmount + increase).toUint128();
        } else {
            // Markdown decreasing (borrower repaying/recovering)
            uint256 decrease = uint256(-markdownDelta);

            // Cap at previously marked down amount to avoid creating phantom supply
            if (decrease > m.totalMarkdownAmount) {
                decrease = m.totalMarkdownAmount;
            }

            // Restore the supply and reduce the tracked markdown amount
            m.totalSupplyAssets = (m.totalSupplyAssets + decrease).toUint128();
            m.totalMarkdownAmount = (m.totalMarkdownAmount - decrease).toUint128();
        }

        market[id] = m;
    }

    /// @notice Get borrower's current borrow assets
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return assets Current borrow amount in assets
    function _getBorrowerAssets(Id id, address borrower) internal view returns (uint256 assets) {
        Market memory m = market[id];
        assets = uint256(position[id][borrower].borrowShares).toAssetsUp(m.totalBorrowAssets, m.totalBorrowShares);
    }

    /// @notice Check if a market is frozen based on cycle timing
    /// @param id Market ID
    /// @return Whether the market is frozen
    function _isMarketFrozen(Id id) internal view returns (bool) {
        uint256 cycleCount = paymentCycle[id].length;
        uint256 cycleDuration = IProtocolConfig(protocolConfig).getCycleDuration();

        if (cycleCount == 0 || cycleDuration == 0) {
            return true;
        }

        uint256 lastCycleEnd = paymentCycle[id][cycleCount - 1].endDate;
        uint256 expectedNextCycleEnd = lastCycleEnd + cycleDuration;

        return block.timestamp >= expectedNextCycleEnd;
    }

    /// @notice Settle a borrower's account by writing off all remaining debt
    /// @dev Only callable by credit line contract
    /// @dev Should be called after any partial repayments have been made
    /// @param marketParams The market parameters
    /// @param borrower The borrower whose account to settle
    /// @return writtenOffAssets Amount of assets written off
    /// @return writtenOffShares Amount of shares written off
    function settleAccount(MarketParams memory marketParams, address borrower)
        external
        onlyCreditLine(marketParams.id())
        returns (uint256 writtenOffAssets, uint256 writtenOffShares)
    {
        Id id = marketParams.id();

        _accrueInterest(marketParams, id);
        _accrueBorrowerPremium(id, borrower);

        // Get position
        writtenOffShares = position[id][borrower].borrowShares;

        // If no debt remains (e.g., insurance fully covered it), still clear all state
        // This prevents settled borrowers from borrowing again
        if (writtenOffShares == 0) {
            // Clear all borrower state to prevent re-borrowing
            _applySettlement(id, borrower, 0, 0);
            emit EventsLib.AccountSettled(id, msg.sender, borrower, 0, 0);
            return (0, 0);
        }

        Market memory m = market[id];

        // Calculate written off assets
        writtenOffAssets = writtenOffShares.toAssetsUp(m.totalBorrowAssets, m.totalBorrowShares);

        // Slash all remaining JANE on settlement
        address manager = ICreditLine(idToMarketParams[id].creditLine).mm();
        if (manager != address(0)) {
            IMarkdownController(manager).slashJaneFull(borrower);
        }

        // Clear position and apply supply adjustment
        _applySettlement(id, borrower, writtenOffShares, writtenOffAssets);

        emit EventsLib.AccountSettled(id, msg.sender, borrower, writtenOffAssets, writtenOffShares);
    }

    /// @notice Apply settlement to storage
    function _applySettlement(Id id, address borrower, uint256 writtenOffShares, uint256 writtenOffAssets) internal {
        uint256 lastMarkdown = markdownState[id][borrower].lastCalculatedMarkdown;

        // Clear borrower position and related state
        position[id][borrower].borrowShares = 0;
        position[id][borrower].collateral = 0;
        delete markdownState[id][borrower];
        delete repaymentObligation[id][borrower];
        delete borrowerPremium[id][borrower];

        // Update borrow totals
        market[id].totalBorrowShares = (market[id].totalBorrowShares - writtenOffShares).toUint128();
        market[id].totalBorrowAssets = (market[id].totalBorrowAssets - writtenOffAssets).toUint128();

        // Apply net supply adjustment
        uint128 totalSupplyAssets = market[id].totalSupplyAssets;
        int256 netAdjustment = int256(lastMarkdown) - int256(writtenOffAssets);
        if (netAdjustment > 0) {
            market[id].totalSupplyAssets = (totalSupplyAssets + uint256(netAdjustment)).toUint128();
        } else if (netAdjustment < 0) {
            uint256 loss = uint256(-netAdjustment);
            if (totalSupplyAssets < loss) revert ErrorsLib.InsufficientLiquidity();
            market[id].totalSupplyAssets = (totalSupplyAssets - loss).toUint128();
        }

        // Update markdown total
        if (lastMarkdown > 0) {
            uint128 totalMarkdownAmount = market[id].totalMarkdownAmount;
            market[id].totalMarkdownAmount =
                totalMarkdownAmount > lastMarkdown ? (totalMarkdownAmount - lastMarkdown).toUint128() : 0;
        }
    }
}
"
    },
    "src/interfaces/IMorpho.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.18;

type Id is bytes32;

struct MarketParams {
    address loanToken;
    address collateralToken;
    address oracle;
    address irm;
    uint256 lltv;
    address creditLine;
}

/// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest
/// accrual.
struct Position {
    uint256 supplyShares;
    uint128 borrowShares;
    uint128 collateral;
}

/// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual.
/// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual.
/// @dev Warning: `totalSupplyShares` does not contain the additional shares accrued by `feeRecipient` since the last
/// interest accrual.
/// @dev Warning: `totalMarkdownAmount` may be stale as markdowns are only updated when borrowers are touched.
struct Market {
    uint128 totalSupplyAssets;
    uint128 totalSupplyShares;
    uint128 totalBorrowAssets;
    uint128 totalBorrowShares;
    uint128 lastUpdate;
    uint128 fee;
    uint128 totalMarkdownAmount; // Running tally of all borrower markdowns
}

/// @notice Per-borrower premium tracking
/// @param lastAccrualTime Timestamp of the last premium accrual for this borrower
/// @param rate Current risk premium rate per second (scaled by WAD)
/// @param borrowAssetsAtLastAccrual Snapshot of borrow position at last premium accrual
struct BorrowerPremium {
    uint128 lastAccrualTime;
    uint128 rate;
    uint128 borrowAssetsAtLastAccrual;
}

/// @notice Repayment tracking structures
enum RepaymentStatus {
    Current,
    GracePeriod,
    Delinquent,
    Default
}

struct PaymentCycle {
    uint256 endDate;
}

struct RepaymentObligation {
    uint128 paymentCycleId;
    uint128 amountDue;
    uint128 endingBalance;
}

/// @notice Markdown state for tracking defaulted debt value reduction
/// @param lastCalculatedMarkdown Last calculated markdown amount
struct MarkdownState {
    uint128 lastCalculatedMarkdown;
}

struct Authorization {
    address authorizer;
    address authorized;
    bool isAuthorized;
    uint256 nonce;
    uint256 deadline;
}

struct Signature {
    uint8 v;
    bytes32 r;
    bytes32 s;
}

/// @dev This interface is used for factorizing IMorphoStaticTyping and IMorpho.
/// @dev Consider using the IMorpho interface instead of this one.
interface IMorphoBase {
    /// @notice The EIP-712 domain separator.
    /// @dev Warning: Every EIP-712 signed message based on this domain separator can be reused on chains sharing the
    /// same chain id and on forks because the domain separator would be the same.
    function DOMAIN_SEPARATOR() external view returns (bytes32);

    /// @notice The owner of the contract.
    /// @dev It has the power to change the owner.
    /// @dev It has the power to set fees on markets and set the fee recipient.
    /// @dev It has the power to enable but not disable IRMs and LLTVs.
    function owner() external view returns (address);

    /// @notice The fee recipient of all markets.
    /// @dev The recipient receives the fees of a given market through a supply position on that market.
    function feeRecipient() external view returns (address);

    /// @notice Whether the `irm` is enabled.
    function isIrmEnabled(address irm) external view returns (bool);

    /// @notice Whether the `lltv` is enabled.
    function isLltvEnabled(uint256 lltv) external view returns (bool);

    /// @notice The `authorizer`'s current nonce. Used to prevent replay attacks with EIP-712 signatures.
    function nonce(address authorizer) external view returns (uint256);

    /// @notice Sets `newOwner` as `owner` of the contract.
    /// @dev Warning: No two-step transfer ownership.
    /// @dev Warning: The owner can be set to the zero address.
    function setOwner(address newOwner) external;

    /// @notice Enables `irm` as a possible IRM for market creation.
    /// @dev Warning: It is not possible to disable an IRM.
    function enableIrm(address irm) external;

    /// @notice Enables `lltv` as a possible LLTV for market creation.
    /// @dev Warning: It is not possible to disable a LLTV.
    function enableLltv(uint256 lltv) external;

    /// @notice Sets the `newFee` for the given market `marketParams`.
    /// @param newFee The new fee, scaled by WAD.
    /// @dev Warning: The recipient can be the zero address.
    function setFee(MarketParams memory marketParams, uint256 newFee) external;

    /// @notice Sets `newFeeRecipient` as `feeRecipient` of the fee.
    /// @dev Warning: If the fee recipient is set to the zero address, fees will accrue there and will be lost.
    /// @dev Modifying the fee recipient will allow the new recipient to claim any pending fees not yet accrued. To
    /// ensure that the current recipient receives all due fees, accrue interest manually prior to making any changes.
    function setFeeRecipient(address newFeeRecipient) external;

    /// @notice Creates the market `marketParams`.
    /// @dev Here is the list of assumptions on the market's dependencies (tokens, IRM and oracle) that guarantees
    /// Morpho behaves as expected:
    /// - The token should be ERC-20 compliant, except that it can omit return values on `transfer` and `transferFrom`.
    /// - The token balance of Morpho should only decrease on `transfer` and `transferFrom`. In particular, tokens with
    /// burn functions are not supported.
    /// - The token should not re-enter Morpho on `transfer` nor `transferFrom`.
    /// - The token balance of the sender (resp. receiver) should decrease (resp. increase) by exactly the given amount
    /// on `transfer` and `transferFrom`. In particular, tokens with fees on transfer are not supported.
    /// - The IRM should not re-enter Morpho.
    /// - The oracle should return a price with the correct scaling.
    /// @dev Here is a list of assumptions on the market's dependencies which, if broken, could break Morpho's liveness
    /// properties (funds could get stuck):
    /// - The token should not revert on `transfer` and `transferFrom` if balances and approvals are right.
    /// - The amount of assets supplied and borrowed should not go above ~1e35 (otherwise the computation of
    /// `toSharesUp` and `toSharesDown` can overflow).
    /// - The IRM should not revert on `borrowRate`.
    /// - The IRM should not return a very high borrow rate (otherwise the computation of `interest` in
    /// `_accrueInterest` can overflow).
    /// - The oracle should not revert `price`.
    /// - The oracle should not return a very high price (otherwise the computation of `maxBorrow` in `_isHealthy` or of
    /// `assetsRepaid` in `liquidate` can overflow).
    /// @dev The borrow share price of a market with less than 1e4 assets borrowed can be decreased by manipulations, to
    /// the point where `totalBorrowShares` is very large and borrowing overflows.
    function createMarket(MarketParams memory marketParams) external;

    /// @notice Supplies `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's
    /// `onMorphoSupply` function with the given `data`.
    /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the
    /// caller is guaranteed to have `assets` tokens pulled from their balance, but the possibility to mint a specific
    /// amount of shares is given for full compatibility and precision.
    /// @dev Supplying a large amount can revert for overflow.
    /// @dev Supplying an amount of shares may lead to supply more or fewer assets than expected due to slippage.
    /// Consider using the `assets` parameter to avoid this.
    /// @param marketParams The market to supply assets to.
    /// @param assets The amount of assets to supply.
    /// @param shares The amount of shares to mint.
    /// @param onBehalf The address that will own the increased supply position.
    /// @param data Arbitrary data to pass to the `onMorphoSupply` callback. Pass empty data if not needed.
    /// @return assetsSupplied The amount of assets supplied.
    /// @return sharesSupplied The amount of shares minted.
    function supply(
        MarketParams memory marketParams,
        uint256 assets,
        uint256 shares,
        address onBehalf,
        bytes memory data
    ) external returns (uint256 assetsSupplied, uint256 sharesSupplied);

    /// @notice Withdraws `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`.
    /// @dev Either `assets` or `shares` should be zero. To withdraw max, pass the `shares`'s balance of `onBehalf`.
    /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions.
    /// @dev Withdrawing an amount corresponding to more shares than supplied will revert for underflow.
    /// @dev It is advised to use the `shares` input when withdrawing the full position to avoid reverts due to
    /// conversion roundings between shares and assets.
    /// @param marketParams The market to withdraw assets from.
    /// @param assets The amount of assets to withdraw.
    /// @param shares The amount of shares to burn.
    /// @param onBehalf The address of the owner of the supply position.
    /// @param receiver The address that will receive the withdrawn assets.
    /// @return assetsWithdrawn The amount of assets withdrawn.
    /// @return sharesWithdrawn The amount of shares burned.
    function withdraw(
        MarketParams memory marketParams,
        uint256 assets,
        uint256 shares,
        address onBehalf,
        address receiver
    ) external returns (uint256 assetsWithdrawn, uint256 sharesWithdrawn);

    /// @notice Borrows `assets` or `shares` on behalf of `onBehalf` and sends the assets to `receiver`.
    /// @dev Either `assets` or `shares` should be zero. Most use cases should rely on `assets` as an input so the
    /// caller is guaranteed to borrow `assets` of tokens, but the possibility to mint a specific amount of shares is
    /// given for full compatibility and precision.
    /// @dev `msg.sender` must be authorized to manage `onBehalf`'s positions.
    /// @dev Borrowing a large amount can revert for overflow.
    /// @dev Borrowing an amount of shares may lead to borrow fewer assets than expected due to slippage.
    /// Consider using the `assets` parameter to avoid this.
    /// @param marketParams The market to borrow assets from.
    /// @param assets The amount of assets to borrow.
    /// @param shares The amount of shares to mint.
    /// @param onBehalf The address that will own the increased borrow position.
    /// @param receiver The address that will receive the borrowed assets.
    /// @return assetsBorrowed The amount of assets borrowed.
    /// @return sharesBorrowed The amount of shares minted.
    function borrow(
        MarketParams memory marketParams,
        uint256 assets,
        uint256 shares,
        address onBehalf,
        address receiver
    ) external returns (uint256 assetsBorrowed, uint256 sharesBorrowed);

    /// @notice Repays `assets` or `shares` on behalf of `onBehalf`, optionally calling back the caller's
    /// `onMorphoRepay` function with the given `data`.
    /// @dev Either `assets` or `shares` should be zero. To repay max, pass the `shares`'s balance of `onBehalf`.
    /// @dev Repaying an amount corresponding to more shares than borrowed will revert for underflow.
    /// @dev It is advised to use the `shares` input when repaying the full position to avoid reverts due to conversion
    /// roundings between shares and assets.
    /// @dev An attacker can front-run a repay with a small repay making the transaction revert for underflow.
    /// @param marketParams The market to repay assets to.
    /// @param assets The amount of assets to repay.
    /// @param shares The amount of shares to burn.
    /// @param onBehalf The address of the owner of the debt position.
    /// @param data Arbitrary data to pass to the `onMorphoRepay` callback. Pass empty data if not needed.
    /// @return assetsRepaid The amount of assets repaid.
    /// @return sharesRepaid The amount of shares burned.
    function repay(
        MarketParams memory marketParams,
        uint256 assets,
        uint256 shares,
        address onBehalf,
        bytes memory data
    ) external returns (uint256 assetsRepaid, uint256 sharesRepaid);

    /// @notice Accrues interest for the given market `marketParams`.
    function accrueInterest(MarketParams memory marketParams) external;

    /// @notice Returns the data stored on the different `slots`.
    function extSloads(bytes32[] memory slots) external view returns (bytes32[] memory);
}

/// @dev This interface is inherited by Morpho so that function signatures are checked by the compiler.
/// @dev Consider using the IMorpho interface instead of this one.
interface IMorphoStaticTyping is IMorphoBase {
    /// @notice The state of the position of `user` on the market corresponding to `id`.
    /// @dev Warning: For `feeRecipient`, `supplyShares` does not contain the accrued shares since the last interest
    /// accrual.
    function position(Id id, address user)
        external
        view
        returns (uint256 supplyShares, uint128 borrowShares, uint128 collateral);

    /// @notice The state of the market corresponding to `id`.
    /// @dev Warning: `totalSupplyAssets` does not contain the accrued interest since the last interest accrual.
    /// @dev Warning: `totalBorrowAssets` does not contain the accrued interest since the last interest accrual.
    /// @dev Warning: `totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last interest
    /// accrual.
    function market(Id id)
        external
        view
        returns (
            uint128 totalSupplyAssets,
            uint128 totalSupplyShares,
            uint128 totalBorrowAssets,
            uint128 totalBorrowShares,
            uint128 lastUpdate,
            uint128 fee,
            uint128 totalMarkdownAmount
        );

    /// @notice The market params corresponding to `id`.
    /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer
    /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`.
    function idToMarketParams(Id id)
        external
        view
        returns (
            address loanToken,
            address collateralToken,
            address oracle,
            address irm,
            uint256 lltv,
            address creditLine
        );
}

/// @title IMorpho
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @dev Use this interface for Morpho to have access to all the functions with the appropriate function signatures.
interface IMorpho is IMorphoBase {
    /// @notice The state of the position of `user` on the market corresponding to `id`.
    /// @dev Warning: For `feeRecipient`, `p.supplyShares` does not contain the accrued shares since the last interest
    /// accrual.
    function position(Id id, address user) external view returns (Position memory p);

    /// @notice The state of the market corresponding to `id`.
    /// @dev Warning: `m.totalSupplyAssets` does not contain the accrued interest since the last interest accrual.
    /// @dev Warning: `m.totalBorrowAssets` does not contain the accrued interest since the last interest accrual.
    /// @dev Warning: `m.totalSupplyShares` does not contain the accrued shares by `feeRecipient` since the last
    /// interest accrual.
    function market(Id id) external view returns (Market memory m);

    /// @notice The market params corresponding to `id`.
    /// @dev This mapping is not used in Morpho. It is there to enable reducing the cost associated to calldata on layer
    /// 2s by creating a wrapper contract with functions that take `id` as input instead of `marketParams`.
    function idToMarketParams(Id id) external view returns (MarketParams memory);
}

/// @title IMorphoCredit
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @dev Use this interface for Morpho to have access to all the functions with the appropriate function signatures.
interface IMorphoCredit {
    /// @notice The helper of the contract.
    function helper() external view returns (address);

    /// @notice The usd3 contract
    function usd3() external view returns (address);

    /// @notice The protocol config of the contract.
    function protocolConfig() external view returns (address);

    /// @notice Sets `helper` as `helper` of the contract.
    /// @param newHelper The new helper address
    function setHelper(address newHelper) external;

    /// @notice Sets `usd3` as `usd3` of the contract.
    /// @param newUsd3 The new usd3 address
    function setUsd3(address newUsd3) external;

    /// @notice Sets the credit line and premium rate for a borrower
    /// @param id The market ID
    /// @param borrower The borrower address
    /// @param credit The credit line amount
    /// @param drp The drp per second in WAD
    function setCreditLine(Id id, address borrower, uint256 credit, uint128 drp) external;

    /// @notice Returns the premium data for a specific borrower in a market
    /// @param id The market ID
    /// @param borrower The borrower address
    /// @return lastAccrualTime Timestamp of the last premium accrual
    /// @return rate Current risk premium rate per second (scaled by WAD)
    /// @return borrowAssetsAtLastAccrual Snapshot of borrow position at last premium accrual
    function borrowerPremium(Id id, address borrower)
        external
        view
        returns (uint128 lastAccrualTime, uint128 rate, uint128 borrowAssetsAtLastAccrual);

    /// @notice Batch accrue premiums for multiple borrowers
    /// @param id Market ID
    /// @param borrowers Array of borrower addresses
    /// @dev Gas usage scales linearly with array size. Callers should manage batch sizes based on block gas limits.
    function accruePremiumsForBorrowers(Id id, address[] calldata borrowers) external;

    /// @notice Close a payment cycle and post obligations for multiple borrowers
    /// @param id Market ID
    /// @param endDate Cycle end date
    /// @param borrowers Array of borrower addresses
    /// @param repaymentBps Array of repayment basis points (e.g., 500 = 5%)
    /// @param endingBalances Array of ending balances for penalty calculations
    function closeCycleAndPostObligations(
        Id id,
        uint256 endDate,
        address[] calldata borrowers,
        uint256[] calldata repaymentBps,
        uint256[] calldata endingBalances
    ) external;

    /// @notice Add obligations to the latest payment cycle
    /// @param id Market ID
    /// @param borrowers Array of borrower addresses
    /// @param repaymentBps Array of repayment basis points (e.g., 500 = 5%)
    /// @param endingBalances Array of ending balances
    function addObligationsToLatestCycle(
        Id id,
        address[] calldata borrowers,
        uint256[] calldata repaymentBps,
        uint256[] calldata endingBalances
    ) external;

    /// @notice Get repayment obligation for a borrower
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return cycleId The payment cycle ID
    /// @return amountDue The amount due
    /// @return endingBalance The ending balance for penalty calculations
    function repaymentObligation(Id id, address borrower)
        external
        view
        returns (uint128 cycleId, uint128 amountDue, uint128 endingBalance);

    /// @notice Get payment cycle end date
    /// @param id Market ID
    /// @param cycleId Cycle ID
    /// @return endDate The cycle end date
    function paymentCycle(Id id, uint256 cycleId) external view returns (uint256 endDate);

    /// @notice Settle a borrower's account by writing off all remaining debt
    /// @dev Only callable by credit line contract
    /// @dev Should be called after any partial repayments have been made
    /// @param marketParams The market parameters
    /// @param borrower The borrower whose account to settle
    /// @return writtenOffAssets Amount of assets written off
    /// @return writtenOffShares Amount of shares written off
    function settleAccount(MarketParams memory marketParams, address borrower)
        external
        returns (uint256 writtenOffAssets, uint256 writtenOffShares);

    /// @notice Get markdown state for a borrower
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return lastCalculatedMarkdown Last calculated markdown amount
    function markdownState(Id id, address borrower) external view returns (uint128 lastCalculatedMarkdown);
}
"
    },
    "src/interfaces/IMarkdownController.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

import {Id} from "./IMorpho.sol";

/// @title IMarkdownController
/// @notice Interface for controlling debt markdowns and JANE token redistribution for borrowers in default
interface IMarkdownController {
    /// @notice Calculate the markdown amount for a borrower's position
    /// @param borrower The address of the borrower
    /// @param borrowAmount The current borrow amount in assets
    /// @param timeInDefault The duration in seconds since the borrower entered default
    /// @return markdownAmount The amount to reduce from the face value
    function calculateMarkdown(address borrower, uint256 borrowAmount, uint256 timeInDefault)
        external
        view
        returns (uint256 markdownAmount);

    /// @notice Get the markdown multiplier for a given time in default
    /// @param timeInDefault The duration in seconds since the borrower entered default
    /// @return multiplier The value multiplier (1e18 = 100% value, 0 = 0% value)
    function getMarkdownMultiplier(uint256 timeInDefault) external view returns (uint256 multiplier);

    /// @notice Check if a borrower's JANE transfers are frozen
    /// @param borrower The borrower address
    /// @return True if the borrower is frozen
    function isFrozen(address borrower) external view returns (bool);

    /// @notice Redistributes JANE proportionally to markdown progression
    /// @param borrower The borrower address
    /// @param timeInDefault Time the borrower has been in default
    /// @return slashed Amount of JANE redistributed
    function slashJaneProportional(address borrower, uint256 timeInDefault) external returns (uint256 slashed);

    /// @notice Redistributes all remaining JANE on settlement
    /// @param borrower The borrower address
    /// @return slashed Amount of JANE redistributed
    function slashJaneFull(address borrower) external returns (uint256 slashed);

    /// @notice Reset burn tracking state for a borrower
    /// @param borrower The borrower address
    function resetBorrowerState(address borrower) external;
}
"
    },
    "src/interfaces/IERC20.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

/// @title IERC20
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @dev Empty because we only call library functions. It prevents calling transfer (transferFrom) instead of
/// safeTransfer (safeTransferFrom).
interface IERC20 {
    function approve(address spender, uint256 value) external returns (bool);
}
"
    },
    "src/interfaces/IProtocolConfig.sol": {
      "content": "// SPDX-License-Identifier: GPL-20later
pragma solidity ^0.8.18;

/// @notice Interface for the ProtocolConfig contract

// Struct to hold market parameters
struct MarketConfig {
    uint256 gracePeriod; // Duration of grace period after cycle end
    uint256 delinquencyPeriod; // Duration of delinquency period before default
    uint256 minBorrow; // Minimum outstanding loan balance to prevent dust
    uint256 irp; // Penalty rate per second for delinquent borrowers
}

// Struct to hold credit line parameters
struct CreditLineConfig {
    uint256 maxLTV;
    uint256 maxVV;
    uint256 maxCreditLine;
    uint256 minCreditLine;
    uint256 maxDRP;
}

// Struct to hold IRM parameters
struct IRMConfig {
    uint256 curveSteepness;
    uint256 adjustmentSpeed;
    uint256 targetUtilization;
    uint256 initialRateAtTarget;
    uint256 minRateAtTarget;
    uint256 maxRateAtTarget;
}

/// @notice Struct to hold IRM parameters with int256 types for internal calculations
struct IRMConfigTyped {
    int256 curveSteepness;
    int256 adjustmentSpeed;
    int256 targetUtilization;
    int256 initialRateAtTarget;
    int256 minRateAtTarget;
    int256 maxRateAtTarget;
}

interface IProtocolConfig {
    /// @dev Initialize the contract with the owner
    /// @param newOwner The address of the new owner
    function initialize(address newOwner) external;

    /// @dev Set a configuration value
    /// @param key The configuration key
    /// @param value The configuration value
    function setConfig(bytes32 key, uint256 value) external;

    // Credit Line getters
    /// @dev Get the credit line parameters
    /// @return The credit line parameters
    function getCreditLineConfig() external view returns (CreditLineConfig memory);

    // Market getters
    /// @dev Get the pause status
    /// @return The pause status value
    function getIsPaused() external view returns (uint256);

    /// @dev Get the maximum on credit
    /// @return The max on credit value
    function getMaxOnCredit() external view returns (uint256);

    /// @dev Get the market parameters
    /// @return The market parameters
    function getMarketConfig() external view returns (MarketConfig memory);

    /// @dev Get the cycle duration for payment cycles
    /// @return The cycle duration in seconds
    function getCycleDuration() external view returns (uint256);

    // IRM getters
    /// @dev Get the IRM parameters
    /// @return The IRM parameters
    functio

Tags:
Multisig, Liquidity, Yield, Upgradeable, Multi-Signature, Factory, Oracle|addr:0xb73b45b141a9b593f142791913ff7b4df4b3f402|verified:true|block:23628148|tx:0xd9540be46200065c41b88b3f5ae26b4e8221cdbe7fa07677e5b10a3df2c697c8|first_check:1761228110

Submitted on: 2025-10-23 16:01:53

Comments

Log in to comment.

No comments yet.