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