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
Submitted on: 2025-10-23 16:01:53
Comments
Log in to comment.
No comments yet.