Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
4a483ef
fix unsorted
adhusson May 15, 2026
af74555
Merge branch 'feat/adapter-v2' into unsorted
adhusson May 20, 2026
67c6288
Merge branch 'feat/adapter-v2' into unsorted
adhusson May 20, 2026
7289f34
gas cost reduction with next maturity floor
adhusson May 20, 2026
421143e
fixes
adhusson May 20, 2026
c6e3df7
docs and small tweaks
adhusson May 20, 2026
16eff58
Merge branch 'feat/adapter-v2' into unsorted
adhusson May 20, 2026
86a7832
Restore unordered maturity array after merge
adhusson May 20, 2026
b4bf2fa
Merge branch 'feat/adapter-v2' into unsorted
adhusson May 20, 2026
4f92293
reorder
adhusson May 20, 2026
a8e441b
Merge branch 'feat/adapter-v2' into unsorted
adhusson May 20, 2026
500db9e
reflect comment
adhusson May 20, 2026
595e10e
uint48, reuse index in memory
adhusson May 21, 2026
723145c
simplify
adhusson May 21, 2026
1672f3d
Merge branch 'feat/adapter-v2' of https://github.com/morpho-org/vault…
adhusson May 21, 2026
378ffd7
dynamic casts and clearer var names
adhusson Jun 1, 2026
ab9d6ca
per market growth tracking, tests improvements
adhusson Jun 1, 2026
013ce98
remove useless debt check
adhusson Jun 1, 2026
5ada1ca
simplify
adhusson Jun 1, 2026
121eebd
simplify and no callbacks
adhusson Jun 5, 2026
ead6039
Merge branch 'feat/adapter-v2' of https://github.com/morpho-org/vault…
adhusson Jun 5, 2026
0333965
remove redundant check
adhusson Jun 10, 2026
db969e2
improve credit removal naming and logic
adhusson Jun 10, 2026
4d32420
manual merge
adhusson Jun 10, 2026
bf2c2e8
Merge branch 'feat/adapter-v2' into unsorted
adhusson Jun 10, 2026
efc9aa1
1 slot maturity data
adhusson Jun 12, 2026
53f8c55
Merge remote-tracking branch 'origin/unsorted' into drop-maturity-index
adhusson Jun 12, 2026
5c591ca
restructure accruals
adhusson Jun 14, 2026
96d130d
Merge branch 'feat/adapter-v2' into unsorted
adhusson Jun 14, 2026
1759fc7
rename to udpate duration caps
adhusson Jun 14, 2026
04d803e
Merge branch 'feat/adapter-v2' into unsorted
adhusson Jul 27, 2026
902a8f1
Merge branch 'unsorted' into drop-maturity-index
adhusson Aug 17, 2026
33d2597
add tests
adhusson Aug 17, 2026
7571cf5
force deallocate for id update and sell take
adhusson Aug 17, 2026
1dbcd60
Merge remote-tracking branch 'origin/feat/adapter-v2' into unsorted
adhusson Aug 17, 2026
adeb9f6
Merge remote-tracking branch 'origin/feat/adapter-v2' into unsorted
adhusson Aug 18, 2026
1dd6e5b
Merge branch 'feat/adapter-v2' into unsorted
adhusson Aug 19, 2026
c56053e
Merge branch 'feat/adapter-v2' into unsorted
adhusson Aug 19, 2026
0dc4173
compile midnight adapter unit at 10000 optimizer runs to fit the fact…
adhusson Aug 20, 2026
4a006b7
raise midnight adapter unit to 17000 optimizer runs, the largest 1e3 …
adhusson Aug 20, 2026
2e14ad8
Merge branch 'feat/adapter-v2' into unsorted
adhusson Aug 27, 2026
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
110 changes: 61 additions & 49 deletions src/adapters/MidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {DurationsLib} from "./libraries/DurationsLib.sol";
contract MidnightAdapter is IMidnightAdapter {
using MathLib for uint256;
using MathLib for uint128;
using MathLib for uint120;
using MathLib for int256;
using DurationsLib for bytes32;

Expand All @@ -45,17 +46,20 @@ contract MidnightAdapter is IMidnightAdapter {

/* ACCOUNTING */

/// @dev Takers of offers of the adapter can fill slots with dust takes.
uint8 public constant MAX_PENDING_MATURITIES = 50;

uint128 public totalAssets;
uint128 public currentGrowth;
uint48 public lastUpdate;
/// @dev Maximum steps of an accrual.
/// @dev After accrual, a maturity uses an availability slot iff it has some units and is > now.
/// @dev Takers of offers of the adapter can fill slots with dust takes.
uint8 public constant MAX_PENDING_MATURITIES = 50;
uint8 public availableMaturities = MAX_PENDING_MATURITIES;
uint8 public pendingMaturitiesLength;
/// @dev Used to avoid reading the entire pendingMaturities array most of the time.
uint48 public nextMaturityFloor = type(uint48).max;
/// @dev Unordered array of future maturities where the adapter has credit.
/// @dev Elements at index >= pendingMaturitiesLength should be ignored.
uint48[MAX_PENDING_MATURITIES] public pendingMaturities;
mapping(uint256 timestamp => MaturityData) public _maturities;
mapping(bytes32 marketId => MarketData) public _markets;

/* CONSTRUCTOR */

constructor(address _parentVault, address _midnight, uint256[] memory _durations) {
Expand Down Expand Up @@ -162,46 +166,61 @@ contract MidnightAdapter is IMidnightAdapter {

/* ACCRUAL */

function accrueInterestView() public view returns (uint48, uint128, uint128, uint256) {
if (block.timestamp == lastUpdate) return (_maturities[0].nextMaturity, currentGrowth, totalAssets, 0);
function accrueInterestView() public view returns (uint128, uint256) {
if (block.timestamp == lastUpdate) return (currentGrowth, totalAssets);

uint256 gainedAssets = 0;
uint128 newGrowth = currentGrowth;
uint256 accrueFrom = lastUpdate;
uint48 _firstMaturity = _maturities[0].nextMaturity;
uint256 removedMaturities = 0;

while (_firstMaturity != 0 && _firstMaturity <= block.timestamp) {
gainedAssets += uint256(newGrowth) * (_firstMaturity - accrueFrom);
newGrowth -= _maturities[_firstMaturity].growth;
accrueFrom = _firstMaturity;
_firstMaturity = _maturities[_firstMaturity].nextMaturity;
removedMaturities++;
uint256 newTotalAssets = totalAssets;

if (block.timestamp >= nextMaturityFloor) {
for (uint256 i = pendingMaturitiesLength; i > 0; i--) {
uint48 maturity = pendingMaturities[i - 1];
if (maturity <= block.timestamp) {
newTotalAssets += uint256(_maturities[maturity].growth) * (maturity - lastUpdate);
newGrowth -= _maturities[maturity].growth;
}
}
}
newTotalAssets += uint256(newGrowth) * (block.timestamp - lastUpdate);

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

return (_firstMaturity, newGrowth, (totalAssets + gainedAssets).toUint128(), removedMaturities);
return (newGrowth, newTotalAssets);
}

function accrueInterest() public returns (uint48, uint128, uint256) {
if (block.timestamp == lastUpdate) return (_maturities[0].nextMaturity, currentGrowth, totalAssets);
function accrueInterest() public returns (uint128, uint256) {
if (block.timestamp == lastUpdate) return (currentGrowth, totalAssets);

uint128 newGrowth = currentGrowth;
uint256 newTotalAssets = totalAssets;

if (block.timestamp >= nextMaturityFloor) {
uint48 newMin = type(uint48).max;
for (uint256 i = pendingMaturitiesLength; i > 0; i--) {
uint48 maturity = pendingMaturities[i - 1];
if (maturity <= block.timestamp) {
newTotalAssets += uint256(_maturities[maturity].growth) * (maturity - lastUpdate);
newGrowth -= _maturities[maturity].growth;
emit RemoveMaturity(maturity);
pendingMaturitiesLength--;
pendingMaturities[i - 1] = pendingMaturities[pendingMaturitiesLength];
} else if (maturity < newMin) {
newMin = maturity;
}
}
nextMaturityFloor = newMin;
currentGrowth = newGrowth;
}
newTotalAssets += uint256(newGrowth) * (block.timestamp - lastUpdate);

uint48 newFirstMaturity;
uint256 removedMaturities;
(newFirstMaturity, currentGrowth, totalAssets, removedMaturities) = accrueInterestView();
availableMaturities += uint8(removedMaturities);
_maturities[0].nextMaturity = newFirstMaturity;
_maturities[newFirstMaturity].prevMaturity = 0;
totalAssets = newTotalAssets.toUint128();
lastUpdate = block.timestamp.toUint48();
emit AccrueInterest(currentGrowth, totalAssets);
emit AccrueInterest(newGrowth, newTotalAssets);

return (newFirstMaturity, currentGrowth, totalAssets);
return (newGrowth, newTotalAssets);
}

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

Expand Down Expand Up @@ -321,7 +340,7 @@ contract MidnightAdapter is IMidnightAdapter {

if (timeToMaturity > 0) {
uint256 interest = boughtNetCredit - paidAssets;
uint128 growthIncrease = (interest / timeToMaturity).toUint128();
uint120 growthIncrease = (interest / timeToMaturity).toUint120();
totalAssets += (paidAssets + interest % timeToMaturity).toUint128();
marketData.growth += growthIncrease;
maturityData.growth += growthIncrease;
Expand All @@ -335,17 +354,9 @@ contract MidnightAdapter is IMidnightAdapter {

// Insert the maturity in the list if needed
if (maturityData.netCredit == boughtNetCredit && boughtNetCredit > 0 && market.maturity > block.timestamp) {
availableMaturities--;
uint48 prevMaturity = 0;
uint48 nextMaturity = _maturities[0].nextMaturity;
while (nextMaturity != 0 && nextMaturity < market.maturity) {
prevMaturity = nextMaturity;
nextMaturity = _maturities[prevMaturity].nextMaturity;
}
maturityData.nextMaturity = _maturities[prevMaturity].nextMaturity;
maturityData.prevMaturity = prevMaturity;
_maturities[prevMaturity].nextMaturity = market.maturity.toUint48();
_maturities[maturityData.nextMaturity].prevMaturity = market.maturity.toUint48();
pendingMaturities[pendingMaturitiesLength] = market.maturity.toUint48();
pendingMaturitiesLength++;
if (market.maturity < nextMaturityFloor) nextMaturityFloor = market.maturity.toUint48();
emit InsertMaturity(market.maturity);
}

Expand Down Expand Up @@ -404,7 +415,7 @@ contract MidnightAdapter is IMidnightAdapter {

if (maturity > block.timestamp) {
uint256 timeToMaturity = maturity - block.timestamp;
uint128 growthDecrease = marketData.growth.mulDivUp(netCreditDecrease, marketData.netCredit).toUint128();
uint120 growthDecrease = marketData.growth.mulDivUp(netCreditDecrease, marketData.netCredit).toUint120();
marketData.growth -= growthDecrease;
maturityData.growth -= growthDecrease;
currentGrowth -= growthDecrease;
Expand All @@ -416,10 +427,11 @@ contract MidnightAdapter is IMidnightAdapter {
marketData.netCredit -= netCreditDecrease.toUint128();

if (maturityData.netCredit == 0 && maturity > block.timestamp) {
availableMaturities++;
_maturities[maturityData.prevMaturity].nextMaturity = maturityData.nextMaturity;
_maturities[maturityData.nextMaturity].prevMaturity = maturityData.prevMaturity;
uint256 index;
while (pendingMaturities[index] != maturity) index++;
emit RemoveMaturity(maturity);
pendingMaturitiesLength--;
pendingMaturities[index] = pendingMaturities[pendingMaturitiesLength];
}
}

Expand Down
18 changes: 8 additions & 10 deletions src/adapters/interfaces/IMidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@ import {Market, Offer} from "lib/midnight/src/interfaces/IMidnight.sol";
import {IBuyCallback, ISellCallback} from "lib/midnight/src/interfaces/ICallbacks.sol";
import {IRatifier} from "lib/midnight/src/interfaces/IRatifier.sol";

// Chain of maturities, each can represent multiple markets.
// nextMaturity is 0 if no next maturity.
struct MaturityData {
uint128 netCredit;
uint128 growth;
uint48 prevMaturity;
uint48 nextMaturity;
uint120 growth;
uint8 durationCount;
}

struct MarketData {
uint128 netCredit;
uint128 growth;
uint120 growth;
}

interface IMidnightAdapter is IAdapter, IBuyCallback, ISellCallback, IRatifier {
Expand Down Expand Up @@ -60,13 +56,15 @@ interface IMidnightAdapter is IAdapter, IBuyCallback, ISellCallback, IRatifier {
function asset() external view returns (address);
function totalAssets() external view returns (uint128);
function lastUpdate() external view returns (uint48);
function nextMaturityFloor() external view returns (uint48);
function currentGrowth() external view returns (uint128);
function availableMaturities() external view returns (uint8);
function pendingMaturities(uint256) external view returns (uint48);
function pendingMaturitiesLength() external view returns (uint8);
function MAX_PENDING_MATURITIES() external view returns (uint8);
function midnight() external view returns (address);
function adapterId() external view returns (bytes32);
function packedDurations() external view returns (bytes32);
function _markets(bytes32 marketId) external view returns (uint128 netCredit, uint128 growth);
function _markets(bytes32 marketId) external view returns (uint128 netCredit, uint120 growth);
function maturities(uint256 date) external view returns (MaturityData memory);
function skimRecipient() external view returns (address);
function isRootCanceled(bytes32 root) external view returns (bool);
Expand All @@ -80,8 +78,8 @@ interface IMidnightAdapter is IAdapter, IBuyCallback, ISellCallback, IRatifier {
function take(Offer memory offer, bytes memory ratifierData, uint256 units) external;
function ids(Market memory market) external view returns (bytes32[] memory);
function parentVault() external view returns (address);
function accrueInterestView() external view returns (uint48, uint128, 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 caller)
external
returns (bytes32[] memory, int256);
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/MathLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ library MathLib {
return uint48(x);
}

/// @dev Casts from uint256 to uint120, reverting if input number is too large.
function toUint120(uint256 x) internal pure returns (uint120) {
require(x <= type(uint120).max, ErrorsLib.CastOverflow());
// forge-lint: disable-next-item(unsafe-typecast) safe because x <= type(uint120).max.
return uint120(x);
}

/// @dev Casts from uint256 to uint128, reverting if input number is too large.
function toUint128(uint256 x) internal pure returns (uint128) {
require(x <= type(uint128).max, ErrorsLib.CastOverflow());
Expand Down
Loading
Loading