Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 72 additions & 78 deletions src/adapters/MidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {MathLib} from "../libraries/MathLib.sol";
import {IVaultV2} from "../interfaces/IVaultV2.sol";
import {IMidnightAdapter, MaturityData, IAdapter} from "./interfaces/IMidnightAdapter.sol";
import {DurationsLib} from "./libraries/DurationsLib.sol";
import {MaturitiesLib} from "./libraries/MaturitiesLib.sol";

/// @dev Approximates held assets by linearly accounting for interest separately for each obligation.
/// @dev Losses are immediately accounted minus a discount applied to the remaining interest to be earned, in proportion
Expand All @@ -24,6 +25,8 @@ contract MidnightAdapter is IMidnightAdapter {
using MathLib for uint128;
using MathLib for int256;
using DurationsLib for bytes32;
using MaturitiesLib for uint256;
using MaturitiesLib for uint48;

/* IMMUTABLES */

Expand All @@ -33,6 +36,7 @@ contract MidnightAdapter is IMidnightAdapter {
bytes32 public immutable adapterId;
bytes32 public immutable packedDurations;
uint256 public immutable durationsLength;
uint256 public constant maxTtmWhenBuying = 10 * 52 weeks;

/* MANAGEMENT */

Expand All @@ -42,11 +46,12 @@ contract MidnightAdapter is IMidnightAdapter {

uint256 public _totalAssets;
uint48 public lastUpdate;
uint48 public firstMaturity;
uint128 public currentGrowth;
uint256 public activableMaturities = 50;
mapping(uint256 timestamp => MaturityData) public _maturities;
/// @dev Maturities must be rounded to the next hour.
mapping(uint256 maturity => MaturityData) public maturitiesData;
mapping(bytes32 obligationId => uint256) public netCredit;
mapping(uint256 index => uint256) public bitmaps;
/* CONSTRUCTOR */

constructor(address _parentVault, address _midnight, uint256[] memory _durations) {
Expand All @@ -56,7 +61,6 @@ contract MidnightAdapter is IMidnightAdapter {
lastUpdate = uint48(block.timestamp);
SafeERC20Lib.safeApprove(asset, _midnight, type(uint256).max);
SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max);
firstMaturity = type(uint48).max;
adapterId = keccak256(abi.encode("this", address(this)));

bytes32 _packedDurations;
Expand All @@ -73,7 +77,7 @@ contract MidnightAdapter is IMidnightAdapter {
/* GETTERS */

function maturities(uint256 date) public view returns (MaturityData memory) {
return _maturities[date];
return maturitiesData[date.align()];
}

function durations() public view returns (uint256[] memory) {
Expand Down Expand Up @@ -115,16 +119,19 @@ contract MidnightAdapter is IMidnightAdapter {
accrueInterest();
updateDurationCountAndAllocations(obligation);

if (totalNetCreditDecrease > 0) removeUnits(obligationId, obligation.maturity, totalNetCreditDecrease);
if (totalNetCreditDecrease > 0) {
removeUnits(obligationId, obligation.maturity.align(), totalNetCreditDecrease);
}

int256 change = -int256(totalNetCreditDecrease);
IVaultV2(parentVault).deallocate(address(this), abi.encode(ids(obligation), change), withdrawnAssets);
}

function updateDurationCountAndAllocations(Obligation memory obligation) public {
MaturityData storage maturityData = _maturities[obligation.maturity];
uint256 maturity = obligation.maturity.align();
MaturityData storage maturityData = maturitiesData[maturity];
uint256 oldDurationCount = maturityData.durationCount;
uint256 newDurationCount = durationCount(obligation.maturity);
uint256 newDurationCount = durationCount(maturity);
maturityData.durationCount = uint8(newDurationCount);
// VaultV2.deallocate requires allocation > 0 for each returned id.
if (newDurationCount < oldDurationCount && maturityData.netCredit > 0) {
Expand All @@ -139,36 +146,59 @@ contract MidnightAdapter is IMidnightAdapter {

/* ACCRUAL */

function accrueInterestView() public view returns (uint48, uint128, uint256) {
function accrueInterestView() public view returns (uint128, uint256) {
uint256 lastChange = lastUpdate;
uint48 nextMaturity = firstMaturity;
uint128 newGrowth = currentGrowth;
uint256 gainedAssets;

while (nextMaturity < block.timestamp) {
gainedAssets += uint256(newGrowth) * (nextMaturity - lastChange);
newGrowth -= _maturities[nextMaturity].growth;
lastChange = nextMaturity;
nextMaturity = _maturities[nextMaturity].nextMaturity;
uint256 firstIndex = lastUpdate.bitmapIndex();
uint256 lastIndex = block.timestamp.zeroFloorSub(1).bitmapIndex();
for (uint256 index = firstIndex; index <= lastIndex; index++) {
for (uint256 bitmap = bitmaps[index]; bitmap != 0; bitmap &= bitmap - 1) {
uint256 maturity = index.maturity() + bitmap.lsb() * 1 hours;
if (maturity >= block.timestamp) break;
gainedAssets += uint256(newGrowth) * (maturity - lastChange);
newGrowth -= maturitiesData[maturity].growth;
lastChange = maturity;
}
}

gainedAssets += uint256(newGrowth) * (block.timestamp - lastChange);

return (nextMaturity, newGrowth, _totalAssets + gainedAssets);
return (newGrowth, _totalAssets + gainedAssets);
}

function accrueInterest() public returns (uint48, uint128, uint256) {
function accrueInterest() public returns (uint128, uint256) {
if (lastUpdate != block.timestamp) {
(firstMaturity, currentGrowth, _totalAssets) = accrueInterestView();
uint256 lastChange = lastUpdate;
uint128 newGrowth = currentGrowth;
uint256 gainedAssets;
uint256 firstIndex = lastUpdate.bitmapIndex();
uint256 lastIndex = block.timestamp.zeroFloorSub(1).bitmapIndex();
for (uint256 index = firstIndex; index <= lastIndex; index++) {
uint256 bitmap;
for (bitmap = bitmaps[index]; bitmap != 0; bitmap &= bitmap - 1) {
uint256 maturity = index.maturity() + bitmap.lsb() * 1 hours;
if (maturity >= block.timestamp) break;
gainedAssets += uint256(newGrowth) * (maturity - lastChange);
newGrowth -= maturitiesData[maturity].growth;
lastChange = maturity;
activableMaturities++;
}
bitmaps[index] = bitmap;
}

gainedAssets += uint256(newGrowth) * (block.timestamp - lastChange);
currentGrowth = newGrowth;
_totalAssets += gainedAssets;
lastUpdate = uint48(block.timestamp);
}
return (firstMaturity, currentGrowth, _totalAssets);
return (currentGrowth, _totalAssets);

@adhusson adhusson Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do this as well (it removes the duplication of accrueInterest logic)

Suggested change
return (currentGrowth, _totalAssets);
function accrueInterest() public returns (uint128, uint256) {
if (lastUpdate != block.timestamp) {
(currentGrowth, _totalAssets) = accrueInterestView();
uint256 lastIndex = block.timestamp.zeroFloorSub(1).bitmapIndex();
for (uint256 index = lastUpdate.bitmapIndex(); index < lastIndex; index++) {
bitmaps[index] = 0;
}
uint256 nowBit = (block.timestamp - 1)/ 1 hours% 256 + 1;
bitmaps[lastIndex] = (bitmaps[lastIndex] >> nowBit) << nowBit;
lastUpdate = uint48(block.timestamp);
}
return (currentGrowth, _totalAssets);
}

}

/// @dev Returns an estimate of the real assets assigned to the adapter.
/// @dev Excludes assets reserved for users.
function realAssets() external view returns (uint256) {
(,, uint256 newTotalAssets) = accrueInterestView();
(, uint256 newTotalAssets) = accrueInterestView();
return newTotalAssets;
}

Expand Down Expand Up @@ -216,7 +246,7 @@ contract MidnightAdapter is IMidnightAdapter {
uint256 totalNetCreditDecrease = netCredit[obligationId] - newNetCredit;

if (totalNetCreditDecrease > 0) {
removeUnits(obligationId, offer.obligation.maturity, totalNetCreditDecrease);
removeUnits(obligationId, offer.obligation.maturity.align(), totalNetCreditDecrease);
}

int256 change = -int256(totalNetCreditDecrease);
Expand All @@ -238,8 +268,6 @@ contract MidnightAdapter is IMidnightAdapter {
require(offer.maker == address(this), IncorrectOwner());
require(offer.callback == address(this), IncorrectCallbackAddress());
require(offer.start <= block.timestamp, IncorrectStart());
// uint48.max is the list end pointer
require(offer.obligation.maturity < type(uint48).max, IncorrectMaturity());
require(offer.buy || offer.reduceOnly, NoDebtCreation());

Signature memory sig = abi.decode(data, (Signature));
Expand All @@ -253,27 +281,24 @@ contract MidnightAdapter is IMidnightAdapter {
return CALLBACK_SUCCESS;
}

/// @dev `data` is used for new maturity insertions.
/// @dev It should encode a maturity present in the linked list.
/// @dev That maturity should be earlier than the inserted obligation maturity.
function onBuy(
bytes32 obligationId,
Obligation memory obligation,
address buyer,
uint256 paidAssets,
uint256 boughtCredit,
uint256 buyPendingFeeIncrease,
bytes memory data
bytes memory
) external returns (bytes32) {
uint48 prevMaturity = abi.decode(data, (uint48));
MaturityData storage maturityData = _maturities[obligation.maturity];
uint256 maturity = obligation.maturity.align();
MaturityData storage maturityData = maturitiesData[maturity];
uint256 buyNetCreditIncrease = boughtCredit - buyPendingFeeIncrease;
uint256 timeToMaturity = obligation.maturity.zeroFloorSub(block.timestamp);
uint256 timeToMaturity = maturity.zeroFloorSub(block.timestamp);

require(msg.sender == midnight, NotMidnight());
require(buyer == address(this), NotSelf());
require(prevMaturity < obligation.maturity, IncorrectHint());
require(buyNetCreditIncrease >= paidAssets, BuyAtLoss());
require(timeToMaturity <= maxTtmWhenBuying, MaturityTooFar());

uint256 newNetCredit = IMidnight(midnight).creditOf(obligationId, address(this))
- IMidnight(midnight).pendingFee(obligationId, address(this));
Expand All @@ -285,10 +310,15 @@ contract MidnightAdapter is IMidnightAdapter {
// change is at most buyNetCreditIncrease
if (change < buyNetCreditIncrease.toInt256()) {
uint256 loss = (int256(buyNetCreditIncrease) - change).toUint256();
removeUnits(obligationId, obligation.maturity, loss);
removeUnits(obligationId, maturity, loss);
}

if (maturityData.netCredit == 0 && buyNetCreditIncrease > 0) activableMaturities--;
if (maturityData.netCredit == 0 && buyNetCreditIncrease > 0 && maturity > block.timestamp) {
activableMaturities--;
uint256 index = maturity.bitmapIndex();
uint256 bit = (maturity / 1 hours) % 256;
bitmaps[index] = bitmaps[index] | (uint256(1) << bit);
}

IVaultV2(parentVault).allocate(address(this), abi.encode(ids(obligation), change), paidAssets);

Expand All @@ -304,35 +334,6 @@ contract MidnightAdapter is IMidnightAdapter {
maturityData.netCredit += buyNetCreditIncrease.toUint128();
netCredit[obligationId] += buyNetCreditIncrease.toUint128();

// Insert the maturity in the list if needed
if (obligation.maturity >= block.timestamp) {
uint48 nextMaturity;
if (prevMaturity == 0) {
nextMaturity = firstMaturity;
} else {
require(prevMaturity >= firstMaturity && _maturities[prevMaturity].netCredit > 0, IncorrectHint());
nextMaturity = _maturities[prevMaturity].nextMaturity;
}

while (nextMaturity < obligation.maturity) {
prevMaturity = nextMaturity;
nextMaturity = _maturities[prevMaturity].nextMaturity;
}

if (nextMaturity > obligation.maturity) {
maturityData.prevMaturity = prevMaturity;
maturityData.nextMaturity = nextMaturity;
if (prevMaturity == 0) {
firstMaturity = obligation.maturity.toUint48();
} else {
_maturities[prevMaturity].nextMaturity = obligation.maturity.toUint48();
}
if (nextMaturity < type(uint48).max) {
_maturities[nextMaturity].prevMaturity = obligation.maturity.toUint48();
}
}
}

return CALLBACK_SUCCESS;
}

Expand All @@ -357,7 +358,9 @@ contract MidnightAdapter is IMidnightAdapter {
accrueInterest();
updateDurationCountAndAllocations(obligation);

if (totalNetCreditDecrease > 0) removeUnits(obligationId, obligation.maturity, totalNetCreditDecrease);
if (totalNetCreditDecrease > 0) {
removeUnits(obligationId, obligation.maturity.align(), totalNetCreditDecrease);
}

int256 change = -int256(totalNetCreditDecrease);
IVaultV2(parentVault).deallocate(address(this), abi.encode(ids(obligation), change), sellerAssets);
Expand All @@ -377,7 +380,7 @@ contract MidnightAdapter is IMidnightAdapter {
/// @dev Removes units from tracking.
/// @dev Changes the implied price of the obligation as little as possible.
function removeUnits(bytes32 obligationId, uint256 maturity, uint256 removedUnits) internal {
MaturityData storage maturityData = _maturities[maturity];
MaturityData storage maturityData = maturitiesData[maturity];

if (maturity > block.timestamp) {
uint256 timeToMaturity = maturity - block.timestamp;
Expand All @@ -391,20 +394,11 @@ contract MidnightAdapter is IMidnightAdapter {
maturityData.netCredit -= removedUnits.toUint128();
netCredit[obligationId] -= removedUnits.toUint128();

if (removedUnits > 0 && maturityData.netCredit == 0) {
if (removedUnits > 0 && maturityData.netCredit == 0 && maturity > block.timestamp) {
activableMaturities++;
if (maturity > block.timestamp) {
uint48 prevMaturity = maturityData.prevMaturity;
uint48 nextMaturity = maturityData.nextMaturity;
if (maturity == firstMaturity) {
firstMaturity = nextMaturity;
} else {
_maturities[prevMaturity].nextMaturity = nextMaturity;
}
if (nextMaturity < type(uint48).max) {
_maturities[nextMaturity].prevMaturity = prevMaturity;
}
}
uint256 index = maturity.bitmapIndex();
uint256 bit = (maturity / 1 hours) % 256;
bitmaps[index] = bitmaps[index] & ~(uint256(1) << bit);
}
}

Expand All @@ -414,7 +408,7 @@ contract MidnightAdapter is IMidnightAdapter {
}

function ids(Obligation memory obligation) public view returns (bytes32[] memory) {
uint256 durationsCount = durationCount(obligation.maturity);
uint256 durationsCount = durationCount(obligation.maturity.align());

bytes32[] memory idsArray = new bytes32[](1 + obligation.collateralParams.length * 2 + durationsCount);

Expand Down
14 changes: 5 additions & 9 deletions src/adapters/interfaces/IMidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ import {Obligation} from "lib/midnight/src/interfaces/IMidnight.sol";
import {ICallbacks} from "lib/midnight/src/interfaces/ICallbacks.sol";
import {IRatifier} from "lib/midnight/src/interfaces/IRatifier.sol";

// Chain of maturities, each can represent multiple obligations.
// nextMaturity is type(uint48).max if no next maturity.
struct MaturityData {
uint128 netCredit;
uint128 growth;
uint48 prevMaturity;
uint48 nextMaturity;
uint8 durationCount;
}

Expand All @@ -29,13 +25,12 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier {
error BuyAtLoss();
error IncorrectCallbackAddress();
error IncorrectDuration();
error IncorrectHint();
error IncorrectMaturity();
error IncorrectOffer();
error IncorrectOwner();
error IncorrectSigner();
error IncorrectStart();
error LoanAssetMismatch();
error MaturityTooFar();
error NoBorrowing();
error NoDebtCreation();
error NotAuthorized();
Expand All @@ -48,9 +43,10 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier {
function asset() external view returns (address);
function _totalAssets() external view returns (uint256);
function lastUpdate() external view returns (uint48);
function firstMaturity() external view returns (uint48);
function currentGrowth() external view returns (uint128);
function activableMaturities() external view returns (uint256);
function maxTtmWhenBuying() external view returns (uint256);
function bitmaps(uint256 group) external view returns (uint256);
function midnight() external view returns (address);
function adapterId() external view returns (bytes32);
function packedDurations() external view returns (bytes32);
Expand All @@ -65,8 +61,8 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier {
function withdrawToVault(Obligation memory obligation, uint256 units) external;
function ids(Obligation memory obligation) external view returns (bytes32[] memory);
function parentVault() external view returns (address);
function accrueInterestView() external view returns (uint48, uint128, uint256);
function accrueInterest() external returns (uint48, uint128, uint256);
function accrueInterestView() external view returns (uint128, uint256);
function accrueInterest() external returns (uint128, uint256);
function allocate(bytes memory data, uint256 assets, bytes4, address vaultAllocator)
external
returns (bytes32[] memory, int256);
Expand Down
29 changes: 29 additions & 0 deletions src/adapters/libraries/MaturitiesLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (c) 2025 Morpho Association
pragma solidity ^0.8.0;

library MaturitiesLib {
/// @dev Align maturity to the next hour.
function align(uint256 _maturity) internal pure returns (uint256) {
uint256 h = 1 hours;
return (_maturity + h - 1) / h * h;
}

/// @dev Index of the bitmap containing the already aligned maturity.
function bitmapIndex(uint256 alignedMaturity) internal pure returns (uint256) {
Comment on lines +12 to +13

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the index of a bitmap. Small clash with collateralIndex in midnight which is a bit position.

return alignedMaturity / (256 * 1 hours);
}

/// @dev Earliest maturity in the bitmap at index.
/// @dev Always aligned to an hour.
function maturity(uint256 index) internal pure returns (uint256) {
return index * 256 * 1 hours;
}

/// @dev Least significant set bit. Assumes `bitmap` is not zero.
function lsb(uint256 bitmap) internal pure returns (uint256 res) {
assembly {
res := sub(255, clz(and(bitmap, sub(0, bitmap))))
}
}
}
Loading
Loading