MarkdownController

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/MarkdownController.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.22;

import {IMarkdownController} from "./interfaces/IMarkdownController.sol";
import {IProtocolConfig} from "./interfaces/IProtocolConfig.sol";
import {ProtocolConfigLib} from "./libraries/ProtocolConfigLib.sol";
import {Ownable} from "../lib/openzeppelin/contracts/access/Ownable.sol";
import {Jane} from "./jane/Jane.sol";
import {IMorphoCredit} from "./interfaces/IMorpho.sol";
import {MorphoCreditLib} from "./libraries/periphery/MorphoCreditLib.sol";
import {Id, RepaymentStatus} from "./interfaces/IMorpho.sol";

/// @title MarkdownController
/// @author 3Jane
/// @custom:contact support@3jane.xyz
/// @notice Controls markdown calculations and JANE token redistribution for borrowers in default
/// @dev Markdowns are applied linearly based on time in default and a configurable duration
contract MarkdownController is IMarkdownController, Ownable {
    /// @notice WAD constant for percentage calculations (1e18 = 100%)
    uint256 internal constant WAD = 1e18;

    /// @notice The protocol config contract address
    address public immutable protocolConfig;

    /// @notice The JANE token contract
    Jane public immutable jane;

    /// @notice The MorphoCredit contract address
    address public immutable morphoCredit;

    /// @notice The market ID to check repayment status
    Id public immutable marketId;

    /// @notice Mapping of borrowers with markdown enabled
    mapping(address => bool) public markdownEnabled;

    /// @notice Tracks cumulative JANE slashed per borrower
    mapping(address => uint256) public janeSlashed;

    /// @notice Tracks initial JANE balance when markdown started
    mapping(address => uint256) public initialJaneBalance;

    /// @notice Emitted when markdown is enabled or disabled for a borrower
    /// @param borrower The borrower address
    /// @param enabled Whether markdown is enabled
    event MarkdownEnabledUpdated(address indexed borrower, bool enabled);

    /// @notice Constructor
    /// @param _protocolConfig The protocol config contract address
    /// @param _owner The owner address
    /// @param _jane The JANE token address
    /// @param _morphoCredit The MorphoCredit contract address
    /// @param _marketId The market ID to check repayment status
    constructor(address _protocolConfig, address _owner, address _jane, address _morphoCredit, Id _marketId)
        Ownable(_owner)
    {
        require(_protocolConfig != address(0), "Invalid protocol config");
        require(_jane != address(0), "Invalid jane");
        require(_morphoCredit != address(0), "Invalid morphoCredit");
        protocolConfig = _protocolConfig;
        jane = Jane(_jane);
        morphoCredit = _morphoCredit;
        marketId = _marketId;
    }

    /// @notice Only MorphoCredit can call slash functions
    modifier onlyMorphoCredit() {
        require(msg.sender == morphoCredit, "Only MorphoCredit");
        _;
    }

    /// @notice Get the full markdown duration from protocol config
    /// @return The duration in seconds for 100% markdown
    function fullMarkdownDuration() public view returns (uint256) {
        return IProtocolConfig(protocolConfig).config(ProtocolConfigLib.FULL_MARKDOWN_DURATION);
    }

    /// @notice Enable or disable markdown for a borrower
    /// @param borrower The borrower address
    /// @param enabled Whether to enable markdown
    function setEnableMarkdown(address borrower, bool enabled) external onlyOwner {
        markdownEnabled[borrower] = enabled;
        emit MarkdownEnabledUpdated(borrower, enabled);
    }

    /// @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 . The amount to reduce from the face value
    function calculateMarkdown(address borrower, uint256 borrowAmount, uint256 timeInDefault)
        external
        view
        returns (uint256)
    {
        if (!markdownEnabled[borrower]) {
            return 0;
        }

        uint256 markdownDuration = fullMarkdownDuration();

        if (markdownDuration == 0) {
            return 0;
        }

        if (timeInDefault >= markdownDuration) {
            return borrowAmount;
        }

        return (borrowAmount * timeInDefault) / markdownDuration;
    }

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

        if (markdownDuration == 0 || timeInDefault == 0) {
            return WAD;
        }

        if (timeInDefault >= markdownDuration) {
            return 0;
        }

        uint256 markdownPercentage = (WAD * timeInDefault) / markdownDuration;
        return WAD - markdownPercentage;
    }

    /// @notice Check if a borrower's JANE transfers are frozen
    /// @param borrower The borrower address
    /// @return True if the borrower is frozen (markdown enabled AND delinquent/default)
    function isFrozen(address borrower) external view returns (bool) {
        if (!markdownEnabled[borrower]) return false;

        // Check if borrower is actually delinquent or in default
        (RepaymentStatus status,) = MorphoCreditLib.getRepaymentStatus(IMorphoCredit(morphoCredit), marketId, borrower);
        return status == RepaymentStatus.Delinquent || status == RepaymentStatus.Default;
    }

    /// @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
        onlyMorphoCredit
        returns (uint256 slashed)
    {
        if (!markdownEnabled[borrower] || timeInDefault == 0) return 0;

        uint256 initialBalance = initialJaneBalance[borrower];

        // Initialize tracking on first slash
        if (initialBalance == 0) {
            initialBalance = jane.balanceOf(borrower);
            if (initialBalance == 0) return 0;
            initialJaneBalance[borrower] = initialBalance;
        }

        // Calculate target slash based on initial balance
        uint256 multiplier = getMarkdownMultiplier(timeInDefault);
        uint256 targetSlashed = initialBalance * (WAD - multiplier) / WAD;

        // Slash delta since last touch
        uint256 alreadySlashed = janeSlashed[borrower];
        if (targetSlashed <= alreadySlashed) {
            return 0;
        }

        slashed = targetSlashed - alreadySlashed;

        // Cap at current balance
        uint256 currentBalance = jane.balanceOf(borrower);
        if (slashed > currentBalance) slashed = currentBalance;

        if (slashed > 0) {
            janeSlashed[borrower] += slashed;
            jane.redistributeFromBorrower(borrower, slashed);
        }
    }

    /// @notice Redistributes all remaining JANE on settlement
    /// @param borrower The borrower address
    /// @return slashed Amount of JANE redistributed
    function slashJaneFull(address borrower) external onlyMorphoCredit returns (uint256 slashed) {
        if (!markdownEnabled[borrower]) return 0;

        slashed = jane.balanceOf(borrower);
        if (slashed > 0) {
            jane.redistributeFromBorrower(borrower, slashed);
        }

        // Reset tracking on settlement
        janeSlashed[borrower] = 0;
        initialJaneBalance[borrower] = 0;
    }

    /// @notice Reset burn tracking state for a borrower
    /// @param borrower The borrower address
    function resetBorrowerState(address borrower) external onlyMorphoCredit {
        janeSlashed[borrower] = 0;
        initialJaneBalance[borrower] = 0;
    }
}
"
    },
    "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/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
    function getIRMConfig() external view returns (IRMConfig memory);

    // USD3 & sUSD3 getters
    /// @dev Get the tranche ratio
    /// @return The tranche ratio value
    function getTrancheRatio() external view returns (uint256);

    /// @dev Get the tranche share variant
    /// @return The tranche share variant value
    function getTrancheShareVariant() external view returns (uint256);

    /// @dev Get the SUSD3 lock duration
    /// @return The SUSD3 lock duration value
    function getSusd3LockDuration() external view returns (uint256);

    /// @dev Get the SUSD3 cooldown period
    /// @return The SUSD3 cooldown period value
    function getSusd3CooldownPeriod() external view returns (uint256);

    /// @dev Get the USD3 commitment time
    /// @return The lock period in seconds
    function getUsd3CommitmentTime() external view returns (uint256);

    /// @dev Get the sUSD3 withdrawal window
    /// @return The withdrawal window duration in seconds after cooldown
    function getSusd3WithdrawalWindow() external view returns (uint256);

    /// @dev Get the USD3 supply cap
    /// @return The supply cap in asset units (0 means no cap)
    function getUsd3SupplyCap() external view returns (uint256);

    /// @dev Get configuration value by key
    /// @param key The configuration key
    /// @return The configuration value
    function config(bytes32 key) external view returns (uint256);
}
"
    },
    "src/libraries/ProtocolConfigLib.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

/// @title ProtocolConfigLib
/// @notice Library containing all configuration keys for ProtocolConfig
/// @dev Centralizes all configuration keys to avoid magic strings and improve maintainability
library ProtocolConfigLib {
    // Market Control Keys
    bytes32 internal constant IS_PAUSED = keccak256("IS_PAUSED");
    bytes32 internal constant MAX_ON_CREDIT = keccak256("MAX_ON_CREDIT");
    bytes32 internal constant DEBT_CAP = keccak256("DEBT_CAP");

    // Credit Line Keys
    bytes32 internal constant MIN_LOAN_DURATION = keccak256("MIN_LOAN_DURATION");
    bytes32 internal constant LATE_REPAYMENT_THRESHOLD = keccak256("LATE_REPAYMENT_THRESHOLD");
    bytes32 internal constant DEFAULT_THRESHOLD = keccak256("DEFAULT_THRESHOLD");
    bytes32 internal constant GRACE_PERIOD = keccak256("GRACE_PERIOD");

    // Interest Rate Keys
    bytes32 internal constant MIN_RATE_AT_TARGET = keccak256("MIN_RATE_AT_TARGET");
    bytes32 internal constant MAX_RATE_AT_TARGET = keccak256("MAX_RATE_AT_TARGET");

    // Tranche Keys (USD3 & sUSD3)
    bytes32 internal constant TRANCHE_RATIO = keccak256("TRANCHE_RATIO");
    bytes32 internal constant TRANCHE_SHARE_VARIANT = keccak256("TRANCHE_SHARE_VARIANT");
    bytes32 internal constant MIN_SUSD3_BACKING_RATIO = keccak256("MIN_SUSD3_BACKING_RATIO");

    // Timing Keys
    bytes32 internal constant SUSD3_LOCK_DURATION = keccak256("SUSD3_LOCK_DURATION");
    bytes32 internal constant SUSD3_COOLDOWN_PERIOD = keccak256("SUSD3_COOLDOWN_PERIOD");
    bytes32 internal constant USD3_COMMITMENT_TIME = keccak256("USD3_COMMITMENT_TIME");
    bytes32 internal constant SUSD3_WITHDRAWAL_WINDOW = keccak256("SUSD3_WITHDRAWAL_WINDOW");

    // Supply Cap Keys
    bytes32 internal constant USD3_SUPPLY_CAP = keccak256("USD3_SUPPLY_CAP");

    // Markdown Keys
    bytes32 internal constant FULL_MARKDOWN_DURATION = keccak256("FULL_MARKDOWN_DURATION");
}
"
    },
    "lib/openzeppelin/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "src/jane/Jane.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import {ERC20, ERC20Permit} from "../../lib/openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {AccessControlEnumerable} from "../../lib/openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import {IMarkdownController} from "../interfaces/IMarkdownController.sol";

/**
 * @title Jane
 * @notice 3Jane protocol governance and rewards token with controlled transfer capabilities
 */
contract Jane is ERC20, ERC20Permit, AccessControlEnumerable {
    error TransferNotAllowed();
    error InvalidAddress();
    error Unauthorized();

    event TransferEnabled();
    event MarkdownControllerSet(address indexed controller);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /// @notice Role identifier for the owner (can manage all roles and contract parameters)
    bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");

    /// @notice Role identifier for minters (can mint new tokens before minting is finalized)
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    /// @notice Role identifier for transfer-enabled accounts (can transfer when transfers are disabled)
    bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");

    /// @notice Whether transfers are globally enabled for all users
    /// @dev When true, anyone can transfer. When false, only addresses with transfer role can participate in transfers
    bool public transferable;

    /// @notice MarkdownController that manages transfer freezes for delinquent borrowers
    address public markdownController;

    /// @notice Address that will receive redistributed tokens
    address public distributor;

    /**
     * @notice Initializes the JANE token with owner and distributor
     * @param _initialOwner Address that will be the contract owner
     * @param _distributor Address that will receive redistributed tokens from defaulted borrowers
     */
    constructor(address _initialOwner, address _distributor) ERC20("Jane", "JANE") ERC20Permit("JANE") {
        if (_initialOwner == address(0)) revert InvalidAddress();
        _grantRole(OWNER_ROLE, _initialOwner);
        _setRoleAdmin(MINTER_ROLE, OWNER_ROLE);
        _setRoleAdmin(TRANSFER_ROLE, OWNER_ROLE);
        distributor = _distributor;
    }

    /**
     * @notice Enables transfers globally (one-way switch)
     * @dev Once enabled, transfers cannot be disabled again
     */
    function setTransferable() external onlyRole(OWNER_ROLE) {
        transferable = true;
        emit TransferEnabled();
    }

    /**
     * @notice Sets the MarkdownController address
     * @param _controller Address of the MarkdownController contract
     */
    function setMarkdownController(address _controller) external onlyRole(OWNER_ROLE) {
        markdownController = _controller;
        emit MarkdownControllerSet(_controller);
    }

    /**
     * @notice Renounces the ability to grant MINTER_ROLE (one-way operation)
     * @dev Sets MINTER_ROLE admin to 0 (which no one has)
     * Existing minters can still mint until they individually renounce
     * After this, no new minters can ever be granted
     */
    function renounceMintAdmin() external onlyRole(OWNER_ROLE) {
        _setRoleAdmin(MINTER_ROLE, bytes32(0));
    }

    /**
     * @notice Transfers ownership to a new address atomically
     * @dev Only callable by current owner. Ensures exactly one owner at all times.
     * @param newOwner Address that will become the new owner
     */
    function transferOwnership(address newOwner) external onlyRole(OWNER_ROLE) {
        if (newOwner == address(0)) revert InvalidAddress();
        address previousOwner = _msgSender();
        _revokeRole(OWNER_ROLE, previousOwner);
        _grantRole(OWNER_ROLE, newOwner);
        emit OwnershipTransferred(previousOwner, newOwner);
    }

    /**
     * @inheritdoc ERC20
     * @dev Adds transfer restrictions based on transferable status and transfer roles
     */
    function transfer(address to, uint256 value) public override returns (bool) {
        if (!_canTransfer(_msgSender(), to)) revert TransferNotAllowed();
        return super.transfer(to, value);
    }

    /**
     * @inheritdoc ERC20
     * @dev Adds transfer restrictions based on transferable status and transfer roles
     */
    function transferFrom(address from, address to, uint256 value) public override returns (bool) {
        if (!_canTransfer(from, to)) revert TransferNotAllowed();
        return super.transferFrom(from, to, value);
    }

    /**
     * @notice Mints new tokens to the specified account
     * @dev Only callable by accounts with minter role and before minting is finalized
     * @param account Address to receive the minted tokens
     * @param value Amount of tokens to mint
     */
    function mint(address account, uint256 value) external onlyRole(MINTER_ROLE) {
        if (account == address(0)) revert InvalidAddress();
        _mint(account, value);
    }

    /**
     * @notice Redistributes JANE from defaulted borrower to distributor
     * @dev Only callable by MarkdownController during default/settlement
     * @param borrower Address of the defaulted borrower
     * @param amount Amount of tokens to redistribute
     */
    function redistributeFromBorrower(address borrower, uint256 amount) external {
        if (msg.sender != markdownController) revert Unauthorized();
        if (borrower == address(0) || distributor == address(0)) revert InvalidAddress();
        _transfer(borrower, distributor, amount);
    }

    /**
     * @notice Checks if a transfer is allowed based on current restrictions
     * @dev Internal helper function for transfer validation
     * @param from Address sending tokens
     * @param to Address receiving tokens
     * @return bool True if the transfer is allowed
     */
    function _canTransfer(address from, address to) internal view returns (bool) {
        // First check if transfers are even allowed (cheap checks)
        if (!transferable && !hasRole(TRANSFER_ROLE, from) && !hasRole(TRANSFER_ROLE, to)) {
            return false;
        }

        // Only if transfers would be allowed, check the expensive freeze status
        address _markdownController = markdownController;
        if (_markdownController != address(0)) {
            return !IMarkdownController(_markdownController).isFrozen(from);
        }

        return true;
    }

    /**
     * @notice Returns the current owner address
     * @return The owner address, or address(0) if no owner exists
     */
    function owner() public view returns (address) {
        uint256 count = getRoleMemberCount(OWNER_ROLE);
        return count > 0 ? getRoleMember(OWNER_ROLE, 0) : address(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/libraries/periphery/MorphoCreditLib.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {
    IMorpho,
    IMorphoCredit,
    Id,
    MarketParams,
    BorrowerPremium,
    RepaymentObligation,
    RepaymentStatus,
    MarkdownState
} from "../../interfaces/IMorpho.sol";
import {IMarkdownController} from "../../interfaces/IMarkdownController.sol";
import {IProtocolConfig, MarketConfig} from "../../interfaces/IProtocolConfig.sol";
import {ErrorsLib} from "../ErrorsLib.sol";
import {MorphoLib} from "./MorphoLib.sol";
import {MorphoCreditStorageLib} from "./MorphoCreditStorageLib.sol";
import {MorphoBalancesLib} from "./MorphoBalancesLib.sol";
import {SharesMathLib} from "../SharesMathLib.sol";
import {ICreditLine} from "../../interfaces/ICreditLine.sol";

/// @title MorphoCreditLib
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @notice Helper library to access MorphoCredit storage variables and computed values.
/// @dev This library extends MorphoLib functionality for MorphoCredit-specific features.
library MorphoCreditLib {
    using MorphoLib for IMorpho;
    using MorphoBalancesLib for IMorpho;
    using SharesMathLib for uint256;

    /// @dev Casts IMorphoCredit to IMorpho for accessing base functionality
    function _asIMorpho(IMorphoCredit morpho) private pure returns (IMorpho) {
        return IMorpho(address(morpho));
    }

    /// @notice Get markdown information for a borrower
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return currentMarkdown Current markdown amount (calculated if in default)
    /// @return defaultStartTime When the borrower entered default (0 if not defaulted)
    /// @return borrowAssets Current borrow amount
    function getBorrowerMarkdownInfo(IMorphoCredit morpho, Id id, address borrower)
        internal
        view
        returns (uint256 currentMarkdown, uint256 defaultStartTime, uint256 borrowAssets)
    {
        // Get borrow assets
        IMorpho morphoBase = _asIMorpho(morpho);
        borrowAssets = morphoBase.expectedBorrowAssets(morphoBase.idToMarketParams(id), borrower);

        // Get repayment status
        (RepaymentStatus status, uint256 statusStartTime) = getRepaymentStatus(morpho, id, borrower);

        // Only set defaultStartTime if actually in default status
        if (status == RepaymentStatus.Default) {
            defaultStartTime = statusStartTime;

            // Get markdown manager and calculate markdown if set
            address manager = getMarkdownManager(morpho, id);
            if (manager != address(0) && defaultStartTime > 0 && borrowAssets > 0) {
                uint256 timeInDefault = block.timestamp > defaultStartTime ? block.timestamp - defaultStartTime : 0;
                currentMarkdown = IMarkdownController(manager).calculateMarkdown(borrower, borrowAssets, timeInDefault);
            }
        }
    }

    /// @notice Get total market markdown
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @return totalMarkdown Current total markdown across all borrowers (may be stale)
    function getMarketMarkdownInfo(IMorphoCredit morpho, Id id) internal view returns (uint256 totalMarkdown) {
        // Access totalMarkdownAmount directly from storage
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.marketTotalMarkdownAmountSlot(id);
        totalMarkdown = uint128(uint256(_asIMorpho(morpho).extSloads(slots)[0]));
    }

    /// @notice Get the markdown manager for a market
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @return manager Address of the markdown manager (0 if not set)
    function getMarkdownManager(IMorphoCredit morpho, Id id) internal view returns (address manager) {
        IMorpho morphoBase = _asIMorpho(morpho);
        MarketParams memory marketParams = morphoBase.idToMarketParams(id);
        manager = ICreditLine(marketParams.creditLine).mm();
    }

    /// @notice Get borrower premium details
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return premium The borrower's premium details
    function getBorrowerPremium(IMorphoCredit morpho, Id id, address borrower)
        internal
        view
        returns (BorrowerPremium memory premium)
    {
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.borrowerPremiumSlot(id, borrower);
        bytes32 data = _asIMorpho(morpho).extSloads(slots)[0];

        // BorrowerPremium struct layout:
        // - lastAccrualTime: uint128 (lower 128 bits)
        // - rate: uint128 (upper 128 bits)
        // - borrowAssetsAtLastAccrual: uint128 (next slot, lower 128 bits)
        premium.lastAccrualTime = uint128(uint256(data));
        premium.rate = uint128(uint256(data) >> 128);

        // Get borrowAssetsAtLastAccrual from next slot
        slots[0] = bytes32(uint256(MorphoCreditStorageLib.borrowerPremiumSlot(id, borrower)) + 1);
        premium.borrowAssetsAtLastAccrual = uint128(uint256(_asIMorpho(morpho).extSloads(slots)[0]));
    }

    /// @notice Get repayment obligation for a borrower
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return obligation The repayment obligation details
    function getRepaymentObligation(IMorphoCredit morpho, Id id, address borrower)
        internal
        view
        returns (RepaymentObligation memory obligation)
    {
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.repaymentObligationSlot(id, borrower);
        bytes32 data = _asIMorpho(morpho).extSloads(slots)[0];

        // RepaymentObligation struct layout:
        // - paymentCycleId: uint128 (lower 128 bits)
        // - amountDue: uint128 (upper 128 bits)
        // - endingBalance: uint128 (next slot, lower 128 bits)
        obligation.paymentCycleId = uint128(uint256(data));
        obligation.amountDue = uint128(uint256(data) >> 128);

        // Get endingBalance from next slot
        slots[0] = bytes32(uint256(MorphoCreditStorageLib.repaymentObligationSlot(id, borrower)) + 1);
        obligation.endingBalance = uint128(uint256(_asIMorpho(morpho).extSloads(slots)[0]));
    }

    /// @notice Get markdown state for a borrower
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @param borrower Borrower address
    /// @return lastCalculatedMarkdown The last calculated markdown amount
    function getMarkdownState(IMorphoCredit morpho, Id id, address borrower)
        internal
        view
        returns (uint128 lastCalculatedMarkdown)
    {
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.markdownStateSlot(id, borrower);
        lastCalculatedMarkdown = uint128(uint256(_asIMorpho(morpho).extSloads(slots)[0]));
    }

    /// @notice Get helper address
    /// @param morpho The MorphoCredit instance
    /// @return helper The helper contract address
    function getHelper(IMorphoCredit morpho) internal view returns (address helper) {
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.helperSlot();
        helper = address(uint160(uint256(_asIMorpho(morpho).extSloads(slots)[0])));
    }

    /// @notice Get repayment status for a borrower
    /// @param morpho The MorphoCredit instance
    /// @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(IMorphoCredit morpho, Id id, address borrower)
        internal
        view
        returns (RepaymentStatus status, uint256 statusStartTime)
    {
        // Get repayment obligation
        RepaymentObligation memory obligation = getRepaymentObligation(morpho, id, borrower);

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

        // Get payment cycle length to validate cycleId
        uint256 cycleLength = getPaymentCycleLength(morpho, id);
        if (obligation.paymentCycleId >= cycleLength) return (RepaymentStatus.Current, 0); // Invalid cycle

        // Get cycle end date
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.paymentCycleElementSlot(id, obligation.paymentCycleId);
        uint256 cycleEndDate = uint256(_asIMorpho(morpho).extSloads(slots)[0]);
        statusStartTime = cycleEndDate;

        // Get market config for grace and delinquency periods
        IProtocolConfig protocolConfig = IProtocolConfig(morpho.protocolConfig());
        MarketConfig memory terms = 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);
    }

    /// @notice Get the total number of payment cycles for a market
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @return length The number of payment cycles
    function getPaymentCycleLength(IMorphoCredit morpho, Id id) internal view returns (uint256 length) {
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.paymentCycleLengthSlot(id);
        length = uint256(_asIMorpho(morpho).extSloads(slots)[0]);
    }

    /// @notice Get both start and end dates for a given cycle
    /// @param morpho The MorphoCredit instance
    /// @param id Market ID
    /// @param cycleId Cycle ID
    /// @return startDate The cycle start date
    /// @return endDate The cycle end date
    function getCycleDates(IMorphoCredit morpho, Id id, uint256 cycleId)
        internal
        view
        returns (uint256 startDate, uint256 endDate)
    {
        // Check bounds
        uint256 cycleLength = getPaymentCycleLength(morpho, id);
        if (cycleId >= cycleLength) revert ErrorsLib.InvalidCycleId();

        // Get end date for the requested cycle
        bytes32[] memory slots = new bytes32[](1);
        slots[0] = MorphoCreditStorageLib.paymentCycleElementSlot(id, cycleId);
        endDate = uint256(_asIMorpho(morpho).extSloads(slots)[0]);

        // Get start date (previous cycle's end date + 1 day, or 0 for first cycle)
        if (cycleId != 0) {
            slots[0] = MorphoCreditStorageLib.paymentCycleElementSlot(id, cycleId - 1);
            startDate = uint256(_asIMorpho(morpho).extSloads(slots)[0]) + 1 days;
        }
    }
}
"
    },
    "lib/openzeppelin/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    },
    "lib/openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol)

pragma solidity ^0.8.20;

import {IERC20Permit} from "./IERC20Permit.sol";
import {ERC20} from "../ERC20.sol";
import {ECDSA} from "../../../utils/cryptography/ECDSA.sol";
import {EIP712} from "../../../utils/cryptography/EIP712.sol";
import {Nonces} from "../../../utils/Nonces.sol";

/**
 * @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in
 * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612].
 *
 * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by
 * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't
 * need to send a transaction, and thus is not required to hold Ether at all.
 */
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
    bytes32 private constant PERMIT_TYPEHASH =
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

    /**
     * @dev Permit deadline has expired.
     */
    error ERC2612ExpiredSignature(uint256 deadline);

    /**
     * @dev Mismatched signature.
     */
    error ERC2612InvalidSigner(address signer, address owner);

    /**
     * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
     *
     * It's a good idea to use the same `name` that is defined as the ERC-20 token name.
     */
    constructor(string memory name) EIP712(name, "1") {}

    /**
     * @inheritdoc IERC20Permit
     */
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) public virtual {
        if (block.timestamp > deadline) {
            revert ERC2612ExpiredSignature(deadline);
        }

        bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));

        bytes32 hash = _hashTypedDataV4(structHash);

        address signer = ECDSA.recover(hash, v, r, s);
        if (signer != owner) {
            revert ERC2612InvalidSigner(signer, owner);
        }

        _approve(owner, spender, value);
    }

    /**
     * @inheritdoc IERC20Permit
     */
    function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {
        return super.nonces(owner);
    }

    /**
     * @inheritdoc IERC20Permit
     */
    // solhint-disable-next-line func-name-mixedcase
    function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
        return _domainSeparatorV4();
    }
}
"
    },
    "lib/openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)

pragma solidity ^0.8.20;

import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol";
import {AccessControl} from "../AccessControl.sol";
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";

/**
 * @dev Extension of {AccessControl} that allows enumerating the members of each role.
 */
abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {
    using EnumerableSet for EnumerableSet.AddressSet;

    mapping(bytes32 role => EnumerableSet.AddressSet) private _roleMembers;

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppel

Tags:
ERC20, ERC165, Multisig, Mintable, Swap, Liquidity, Upgradeable, Multi-Signature, Factory, Oracle|addr:0xf0eae71092f3c9411a9eab8f81e7d91d29726214|verified:true|block:23628355|tx:0xf053db2f05bbd2b6a3b86d3bb23f08db783eab615cd43334464b83f12290a43f|first_check:1761229016

Submitted on: 2025-10-23 16:16:59

Comments

Log in to comment.

No comments yet.