From 297067a20a6709856a21bfdf9687ffe7dd6ad84d Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 02:31:15 +0200 Subject: [PATCH 1/6] maturity bitmaps --- src/adapters/MidnightAdapter.sol | 151 +++++++++---------- src/adapters/interfaces/IMidnightAdapter.sol | 21 +-- src/adapters/libraries/MaturitiesLib.sol | 29 ++++ test/MidnightAdapterAllocationUpdateTest.sol | 12 +- test/MidnightAdapterTest.sol | 69 +++------ 5 files changed, 137 insertions(+), 145 deletions(-) create mode 100644 src/adapters/libraries/MaturitiesLib.sol diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 5769a2a70..4a7fdb781 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -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 @@ -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 */ @@ -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 */ @@ -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 group => uint256) public bitmaps; /* CONSTRUCTOR */ constructor(address _parentVault, address _midnight, uint256[] memory _durations) { @@ -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; @@ -72,8 +76,9 @@ contract MidnightAdapter is IMidnightAdapter { /* GETTERS */ + /// @dev Returns the MaturityData for the bucket containing `date` (rounded up to the next hour). function maturities(uint256 date) public view returns (MaturityData memory) { - return _maturities[date]; + return maturitiesData[date.align()]; } function durations() public view returns (uint256[] memory) { @@ -115,16 +120,19 @@ contract MidnightAdapter is IMidnightAdapter { accrueInterest(); updateDurationIndexAndAllocations(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 updateDurationIndexAndAllocations(Obligation memory obligation) public { - MaturityData storage maturityData = _maturities[obligation.maturity]; + uint256 maturity = obligation.maturity.align(); + MaturityData storage maturityData = maturitiesData[maturity]; uint256 oldDurationIndex = maturityData.durationIndex; - uint256 newDurationIndex = durationIndex(obligation.maturity); + uint256 newDurationIndex = durationIndex(maturity); maturityData.durationIndex = uint8(newDurationIndex); // VaultV2.deallocate requires allocation > 0 for each returned id. if (newDurationIndex < oldDurationIndex && maturityData.netCredit > 0) { @@ -139,36 +147,58 @@ 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 firstGroup = lastUpdate.group(); + uint256 lastGroup = block.timestamp.zeroFloorSub(1 hours).group(); + for (uint256 group = firstGroup; group <= lastGroup; group++) { + for (uint256 bitmap = bitmaps[group]; bitmap != 0; bitmap &= bitmap - 1) { + uint256 maturity = group.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 firstGroup = lastUpdate.group(); + uint256 lastGroup = block.timestamp.zeroFloorSub(1 hours).group(); + for (uint256 group = firstGroup; group <= lastGroup; group++) { + uint256 bitmap; + for (bitmap = bitmaps[group]; bitmap != 0; bitmap &= bitmap - 1) { + uint256 maturity = group.maturity() + bitmap.lsb() * 1 hours; + if (maturity >= block.timestamp) break; + gainedAssets += uint256(newGrowth) * (maturity - lastChange); + newGrowth -= maturitiesData[maturity].growth; + lastChange = maturity; + } + bitmaps[group] = bitmap; + } + + gainedAssets += uint256(newGrowth) * (block.timestamp - lastChange); + currentGrowth = newGrowth; + _totalAssets += gainedAssets; lastUpdate = uint48(block.timestamp); } - return (firstMaturity, currentGrowth, _totalAssets); + 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; } @@ -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); @@ -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)); @@ -253,9 +281,6 @@ 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, @@ -263,17 +288,17 @@ contract MidnightAdapter is IMidnightAdapter { 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)); @@ -285,10 +310,16 @@ 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--; + // Fresh activation: bookkeep the slot and, if the maturity is in the future, mark its bitmap bit. + if (maturityData.netCredit == 0 && buyNetCreditIncrease > 0 && maturity > block.timestamp) { + activableMaturities--; + uint256 group = maturity.group(); + uint256 bit = (maturity / 1 hours) % 256; + bitmaps[group] = bitmaps[group] | (uint256(1) << bit); + } IVaultV2(parentVault).allocate(address(this), abi.encode(ids(obligation), change), paidAssets); @@ -304,35 +335,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; } @@ -357,7 +359,9 @@ contract MidnightAdapter is IMidnightAdapter { accrueInterest(); updateDurationIndexAndAllocations(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); @@ -377,7 +381,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; @@ -391,20 +395,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 group = maturity.group(); + uint256 bit = (maturity / 1 hours) % 256; + bitmaps[group] = bitmaps[group] & ~(uint256(1) << bit); } } @@ -414,7 +409,7 @@ contract MidnightAdapter is IMidnightAdapter { } function ids(Obligation memory obligation) public view returns (bytes32[] memory) { - uint256 durationsCount = durationIndex(obligation.maturity); + uint256 durationsCount = durationIndex(obligation.maturity.align()); bytes32[] memory idsArray = new bytes32[](1 + obligation.collateralParams.length * 2 + durationsCount); diff --git a/src/adapters/interfaces/IMidnightAdapter.sol b/src/adapters/interfaces/IMidnightAdapter.sol index ad85c21e9..ada4e28d4 100644 --- a/src/adapters/interfaces/IMidnightAdapter.sol +++ b/src/adapters/interfaces/IMidnightAdapter.sol @@ -7,13 +7,14 @@ 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. +// Per-hour accounting, keyed by `maturityHour` — each obligation's real maturity is rounded up to +// the next MATURITY_PRECISION boundary (so `maturityHour * MATURITY_PRECISION` is the effective +// expiry used by the adapter). An hour's data is "active" iff netCredit > 0 and its effective +// expiry is in the future; the `bitmaps` mapping tracks the latter in a bitmap-per-group layout +// (each `group` holds 256 consecutive maturity hours) for O(1) iteration during accrual. struct MaturityData { uint128 netCredit; uint128 growth; - uint48 prevMaturity; - uint48 nextMaturity; uint8 durationIndex; } @@ -29,13 +30,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(); @@ -48,9 +48,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 activeMaturities() external view returns (uint256); + 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); @@ -65,8 +66,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); diff --git a/src/adapters/libraries/MaturitiesLib.sol b/src/adapters/libraries/MaturitiesLib.sol new file mode 100644 index 000000000..1affde130 --- /dev/null +++ b/src/adapters/libraries/MaturitiesLib.sol @@ -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 Bitmap group containing the already aligned maturity. + function group(uint256 alignedMaturity) internal pure returns (uint256) { + return alignedMaturity / (256 * 1 hours); + } + + /// @dev Starting maturity of a bitmap group. + /// @dev Always aligned to an hour. + function maturity(uint256 _group) internal pure returns (uint256) { + return _group * 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)))) + } + } +} diff --git a/test/MidnightAdapterAllocationUpdateTest.sol b/test/MidnightAdapterAllocationUpdateTest.sol index 549ec98b0..19f6a99cf 100644 --- a/test/MidnightAdapterAllocationUpdateTest.sol +++ b/test/MidnightAdapterAllocationUpdateTest.sol @@ -124,8 +124,8 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { function testExitDuration(uint256 durationIndex, uint256 timeToMaturity, uint256 extraSkip) public { durationIndex = bound(durationIndex, 0, adapter.durationsLength() - 1); uint256 duration = adapter.durations()[durationIndex]; - timeToMaturity = bound(timeToMaturity, duration, type(uint32).max); - extraSkip = bound(extraSkip, 1, 10 * 365 days); + timeToMaturity = bound(timeToMaturity, duration, adapter.maxTtmWhenBuying() - 1 hours); + extraSkip = bound(extraSkip, 1 hours + 1, 10 * 365 days); Offer memory offer = buy(timeToMaturity, 1e18); assertEq(parentVault.allocation(durationId(duration)), 1e18); @@ -142,7 +142,7 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { { durationIndex = bound(durationIndex, 0, adapter.durationsLength() - 1); uint256 duration = adapter.durations()[durationIndex]; - timeToMaturity = bound(timeToMaturity, duration, type(uint32).max); + timeToMaturity = bound(timeToMaturity, duration, adapter.maxTtmWhenBuying() - 1 hours); skipAmount = bound(skipAmount, 0, duration * 2); Offer memory offer = buy(timeToMaturity, 1e18); @@ -174,7 +174,8 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { assertEq(parentVault.allocation(durationId(1 days)), 1e18, "1 day, before"); assertEq(parentVault.allocation(durationId(7 days)), 1e18, "7 days, before"); - skip(1); + // Skip past the 1-hour alignment overshoot so the bucketed TTM crosses the 7-day boundary. + skip(1 hours + 1); parentVault.setTotalAssets(1e18); parentVault.setAdaptersLength(1); @@ -192,7 +193,8 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { assertEq(parentVault.allocation(durationId(1 days)), 1e18, "1 day, before"); assertEq(parentVault.allocation(durationId(7 days)), 1e18, "7 days, before"); - skip(1); + // Skip past the 1-hour alignment overshoot so the bucketed TTM crosses the 7-day boundary. + skip(1 hours + 1); forceDeallocate(offer.obligation, 0.5e18); diff --git a/test/MidnightAdapterTest.sol b/test/MidnightAdapterTest.sol index 4deb4eba9..e7347a6f2 100644 --- a/test/MidnightAdapterTest.sol +++ b/test/MidnightAdapterTest.sol @@ -12,6 +12,7 @@ import {IERC20} from "../src/interfaces/IERC20.sol"; import {IMidnightAdapter} from "../src/adapters/interfaces/IMidnightAdapter.sol"; import {IMidnightAdapterFactory} from "../src/adapters/interfaces/IMidnightAdapterFactory.sol"; import {MathLib} from "../src/libraries/MathLib.sol"; +import {MaturitiesLib} from "../src/adapters/libraries/MaturitiesLib.sol"; import {Midnight} from "../lib/midnight/src/Midnight.sol"; import {IMidnight, Offer, Obligation, CollateralParams} from "../lib/midnight/src/interfaces/IMidnight.sol"; import {Signature, EIP712_DOMAIN_TYPEHASH, ROOT_TYPEHASH} from "../lib/midnight/src/interfaces/IEcrecover.sol"; @@ -30,6 +31,7 @@ struct Step { contract MidnightAdapterTest is Test { using stdStorage for StdStorage; using MathLib for uint256; + using MaturitiesLib for uint256; IMidnight internal midnight; IMidnightAdapterFactory internal factory; @@ -205,18 +207,17 @@ contract MidnightAdapterTest is Test { units, taker, address(0), "", taker, offer, sign([offer], signerAllocator), root([offer]), proof([offer]) ); - uint256 remainder = (units - assets) % (offer.obligation.maturity - vm.getBlockTimestamp()); + uint256 alignedMaturity = offer.obligation.maturity.align(); + uint256 duration = alignedMaturity - vm.getBlockTimestamp(); + uint256 remainder = (units - assets) % duration; assertEq(adapter._totalAssets(), assets + remainder, "_totalAssets"); assertEq(adapter.lastUpdate(), vm.getBlockTimestamp(), "lastUpdate"); - assertEq(adapter.firstMaturity(), vm.getBlockTimestamp() + 200, "firstMaturity"); uint256 totalInterest = units - assets; - uint256 duration = offer.obligation.maturity - vm.getBlockTimestamp(); uint256 newGrowth = totalInterest / duration; assertEq(adapter.currentGrowth(), newGrowth, "currentGrowth"); MaturityData memory maturityData = adapter.maturities(offer.obligation.maturity); assertEq(maturityData.growth, newGrowth, "growth"); - assertEq(maturityData.nextMaturity, type(uint48).max, "nextMaturity"); uint256 actualUnits = adapter.netCredit(_obligationId(offer.obligation)); assertEq(actualUnits, units, "units"); @@ -250,24 +251,19 @@ contract MidnightAdapterTest is Test { units1, taker, address(0), "", taker, offer, sign([offer], signerAllocator), root([offer]), proof([offer]) ); - uint256 timeToMaturity = maturity - block.timestamp; + uint256 alignedMaturity = maturity.align(); + uint256 timeToMaturity = alignedMaturity - block.timestamp; uint128 growth1 = uint128((units1 - assets1) / timeToMaturity); assertGt(growth1, 0, "growth should be nonzero"); assertEq(adapter.currentGrowth(), growth1, "currentGrowth after buy1"); // Step 2: Advance time past maturity M skip(timeToMaturity + 1); - assertGt(block.timestamp, maturity, "should be past maturity"); + assertGt(block.timestamp, alignedMaturity, "should be past aligned maturity"); // Step 3: Trigger accrueInterest so the walk subtracts growth from currentGrowth adapter.accrueInterest(); assertEq(adapter.currentGrowth(), 0, "currentGrowth after accrual should be 0"); - assertEq(adapter.firstMaturity(), type(uint48).max, "firstMaturity should be sentinel"); - - // In midnight, any seller with debt past maturity is always liquidatable - // (isLiquidatable returns true if block.timestamp > maturity && debt > 0), - // so we can't test a second buy at past maturity. Just verify accrual state. - assertEq(adapter.firstMaturity(), type(uint48).max, "past maturity not re-inserted into list"); // Note: In midnight, any seller with debt past maturity is always liquidatable, // so the second buy at past maturity from the original test cannot be executed. @@ -368,16 +364,6 @@ contract MidnightAdapterTest is Test { adapter.onRatify(offer, _root, data); } - function testRatifyIncorrectMaturity(uint256 seed) public { - vm.setSeed(seed); - Offer memory offer = _ratificationSetup(); - offer.obligation.maturity = vm.randomUint(type(uint48).max, type(uint256).max); - bytes32 _root = root(offer); - bytes memory data = ratifierData(_root, signerAllocator); - vm.expectRevert(IMidnightAdapter.IncorrectMaturity.selector); - adapter.onRatify(offer, _root, data); - } - function testRatifyIncorrectStart(uint256 seed) public { vm.setSeed(seed); Offer memory offer = _ratificationSetup(); @@ -442,7 +428,8 @@ contract MidnightAdapterTest is Test { for (uint256 i = 0; i < steps.length; i++) { Step memory step = steps[i]; - uint256 timeToMaturity = step.maturity - vm.getBlockTimestamp(); + uint256 alignedMaturity = step.maturity.align(); + uint256 timeToMaturity = alignedMaturity - vm.getBlockTimestamp(); require(timeToMaturity > 0 || step.approxGrowth == 0, "nonzero growth on 0 duration"); uint256 approxInterest = step.approxGrowth * timeToMaturity; offer.group = bytes32(i); @@ -482,13 +469,13 @@ contract MidnightAdapterTest is Test { assertEq(adapter.netCredit(obligationId), unitsBefore + units, "setup: units 1"); expectedUnits[obligationId] += units; - expectedMaturityGrowths[step.maturity] += actualGrowth; + expectedMaturityGrowths[alignedMaturity] += actualGrowth; if (timeToMaturity > 0) { expectedAddedGrowth += actualGrowth.toUint128(); } expectedAddedAssets += step.assets + zeroPeriodGain; expectedPositionsList.push(uint256(obligationId)); - expectedMaturitiesList.push(step.maturity); + expectedMaturitiesList.push(alignedMaturity); } expectedPositionsList = removeCopies(expectedPositionsList); expectedMaturitiesList = removeCopies(expectedMaturitiesList); @@ -505,31 +492,13 @@ contract MidnightAdapterTest is Test { setupObligations(steps); - // Check pointer to first element of maturities list - if (steps.length > 0) { - assertEq(adapter.firstMaturity(), steps[0].maturity, "firstMaturity"); - } else { - assertEq(adapter.firstMaturity(), type(uint48).max, "firstMaturity"); - } - - // Check maturities growth and linked list structure + // Check maturities growth for (uint256 i = 0; i < expectedMaturitiesList.length; i++) { assertEq( adapter.maturities(expectedMaturitiesList[i]).growth, expectedMaturityGrowths[expectedMaturitiesList[i]], "growth" ); - if (i == expectedMaturitiesList.length - 1) { - assertEq( - adapter.maturities(expectedMaturitiesList[i]).nextMaturity, type(uint48).max, "nextMaturity end" - ); - } else { - assertEq( - adapter.maturities(expectedMaturitiesList[i]).nextMaturity, - expectedMaturitiesList[i + 1], - "nextMaturity middle" - ); - } } // Check positions growth and size @@ -571,11 +540,10 @@ contract MidnightAdapterTest is Test { skip(elapsed); - (uint48 nextMaturity, uint128 newGrowth, uint256 newTotalAssets) = adapter.accrueInterestView(); + (uint128 newGrowth, uint256 newTotalAssets) = adapter.accrueInterestView(); uint256 lostGrowth = 0; uint256 interest = initialGrowth * elapsed; - uint256 expectedNextMaturity = type(uint48).max; for (uint256 i = 0; i < expectedMaturitiesList.length; i++) { uint256 maturity = expectedMaturitiesList[i]; @@ -585,11 +553,7 @@ contract MidnightAdapterTest is Test { } else { interest += expectedMaturityGrowths[maturity] * elapsed; } - if (maturity >= vm.getBlockTimestamp() && maturity < expectedNextMaturity) { - expectedNextMaturity = maturity; - } } - assertEq(nextMaturity, expectedNextMaturity, "nextMaturity"); assertEq(newGrowth, expectedCurrentGrowth - lostGrowth, "newGrowth"); assertEq(newTotalAssets, _totalAssets + expectedAddedAssets + interest, "newTotalAssets"); } @@ -625,7 +589,7 @@ contract MidnightAdapterTest is Test { collateralParams[i].token = address(uint160(i)); } obligation.collateralParams = storedCollaterals; - obligation.maturity = bound(maturity, 1, 700 days); + obligation.maturity = bound(maturity, block.timestamp + 1, block.timestamp + 700 days); bytes32[] memory ids = adapter.ids(obligation); assertEq(ids[0], adapter.adapterId()); @@ -646,8 +610,9 @@ contract MidnightAdapterTest is Test { uint256[] memory durations = adapter.durations(); uint256 durationIdCount = 0; + uint256 alignedTtm = obligation.maturity.align() - block.timestamp; for (uint256 i = 0; i < durations.length; i++) { - if ((obligation.maturity - block.timestamp) >= durations[i]) { + if (alignedTtm >= durations[i]) { assertEq( ids[1 + obligation.collateralParams.length * 2 + durationIdCount], keccak256(abi.encode("duration", durations[i])) From 6eb07b63592e7b6d79d24935a8f5f472c0661b86 Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 02:45:00 +0200 Subject: [PATCH 2/6] docs --- src/adapters/MidnightAdapter.sol | 2 -- src/adapters/interfaces/IMidnightAdapter.sol | 5 ----- 2 files changed, 7 deletions(-) diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 4a7fdb781..e788ef28f 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -76,7 +76,6 @@ contract MidnightAdapter is IMidnightAdapter { /* GETTERS */ - /// @dev Returns the MaturityData for the bucket containing `date` (rounded up to the next hour). function maturities(uint256 date) public view returns (MaturityData memory) { return maturitiesData[date.align()]; } @@ -313,7 +312,6 @@ contract MidnightAdapter is IMidnightAdapter { removeUnits(obligationId, maturity, loss); } - // Fresh activation: bookkeep the slot and, if the maturity is in the future, mark its bitmap bit. if (maturityData.netCredit == 0 && buyNetCreditIncrease > 0 && maturity > block.timestamp) { activableMaturities--; uint256 group = maturity.group(); diff --git a/src/adapters/interfaces/IMidnightAdapter.sol b/src/adapters/interfaces/IMidnightAdapter.sol index ada4e28d4..612b26c6e 100644 --- a/src/adapters/interfaces/IMidnightAdapter.sol +++ b/src/adapters/interfaces/IMidnightAdapter.sol @@ -7,11 +7,6 @@ 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"; -// Per-hour accounting, keyed by `maturityHour` — each obligation's real maturity is rounded up to -// the next MATURITY_PRECISION boundary (so `maturityHour * MATURITY_PRECISION` is the effective -// expiry used by the adapter). An hour's data is "active" iff netCredit > 0 and its effective -// expiry is in the future; the `bitmaps` mapping tracks the latter in a bitmap-per-group layout -// (each `group` holds 256 consecutive maturity hours) for O(1) iteration during accrual. struct MaturityData { uint128 netCredit; uint128 growth; From 51a9dd0284e1189b71b65a0891bba5ebd9b2999a Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 15:54:24 +0200 Subject: [PATCH 3/6] fixes and renames --- src/adapters/MidnightAdapter.sol | 61 ++++++++++---------- src/adapters/interfaces/IMidnightAdapter.sol | 4 +- src/adapters/libraries/MaturitiesLib.sol | 10 ++-- test/MidnightAdapterAllocationUpdateTest.sol | 24 ++++---- 4 files changed, 50 insertions(+), 49 deletions(-) diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index e788ef28f..37c5c4fae 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -51,7 +51,7 @@ contract MidnightAdapter is IMidnightAdapter { /// @dev Maturities must be rounded to the next hour. mapping(uint256 maturity => MaturityData) public maturitiesData; mapping(bytes32 obligationId => uint256) public netCredit; - mapping(uint256 group => uint256) public bitmaps; + mapping(uint256 index => uint256) public bitmaps; /* CONSTRUCTOR */ constructor(address _parentVault, address _midnight, uint256[] memory _durations) { @@ -117,7 +117,7 @@ contract MidnightAdapter is IMidnightAdapter { uint256 totalNetCreditDecrease = netCredit[obligationId] - newNetCredit; accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); if (totalNetCreditDecrease > 0) { removeUnits(obligationId, obligation.maturity.align(), totalNetCreditDecrease); @@ -127,17 +127,17 @@ contract MidnightAdapter is IMidnightAdapter { IVaultV2(parentVault).deallocate(address(this), abi.encode(ids(obligation), change), withdrawnAssets); } - function updateDurationIndexAndAllocations(Obligation memory obligation) public { + function updateDurationCountAndAllocations(Obligation memory obligation) public { uint256 maturity = obligation.maturity.align(); MaturityData storage maturityData = maturitiesData[maturity]; - uint256 oldDurationIndex = maturityData.durationIndex; - uint256 newDurationIndex = durationIndex(maturity); - maturityData.durationIndex = uint8(newDurationIndex); + uint256 olddurationCount = maturityData.durationCount; + uint256 newdurationCount = durationCount(maturity); + maturityData.durationCount = uint8(newdurationCount); // VaultV2.deallocate requires allocation > 0 for each returned id. - if (newDurationIndex < oldDurationIndex && maturityData.netCredit > 0) { - bytes32[] memory zeroedDurationsIds = new bytes32[](oldDurationIndex - newDurationIndex); + if (newdurationCount < olddurationCount && maturityData.netCredit > 0) { + bytes32[] memory zeroedDurationsIds = new bytes32[](olddurationCount - newdurationCount); for (uint256 i = 0; i < zeroedDurationsIds.length; i++) { - zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newDurationIndex + i))); + zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newdurationCount + i))); } int256 change = -int256(uint256(maturityData.netCredit)); IVaultV2(parentVault).deallocate(address(this), abi.encode(zeroedDurationsIds, change), 0); @@ -150,11 +150,11 @@ contract MidnightAdapter is IMidnightAdapter { uint256 lastChange = lastUpdate; uint128 newGrowth = currentGrowth; uint256 gainedAssets; - uint256 firstGroup = lastUpdate.group(); - uint256 lastGroup = block.timestamp.zeroFloorSub(1 hours).group(); - for (uint256 group = firstGroup; group <= lastGroup; group++) { - for (uint256 bitmap = bitmaps[group]; bitmap != 0; bitmap &= bitmap - 1) { - uint256 maturity = group.maturity() + bitmap.lsb() * 1 hours; + 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; @@ -172,18 +172,19 @@ contract MidnightAdapter is IMidnightAdapter { uint256 lastChange = lastUpdate; uint128 newGrowth = currentGrowth; uint256 gainedAssets; - uint256 firstGroup = lastUpdate.group(); - uint256 lastGroup = block.timestamp.zeroFloorSub(1 hours).group(); - for (uint256 group = firstGroup; group <= lastGroup; group++) { + uint256 firstIndex = lastUpdate.bitmapIndex(); + uint256 lastIndex = block.timestamp.zeroFloorSub(1).bitmapIndex(); + for (uint256 index = firstIndex; index <= lastIndex; index++) { uint256 bitmap; - for (bitmap = bitmaps[group]; bitmap != 0; bitmap &= bitmap - 1) { - uint256 maturity = group.maturity() + bitmap.lsb() * 1 hours; + 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[group] = bitmap; + bitmaps[index] = bitmap; } gainedAssets += uint256(newGrowth) * (block.timestamp - lastChange); @@ -238,7 +239,7 @@ contract MidnightAdapter is IMidnightAdapter { require(IMidnight(midnight).debtOf(obligationId, address(this)) == 0, NoBorrowing()); accrueInterest(); - updateDurationIndexAndAllocations(offer.obligation); + updateDurationCountAndAllocations(offer.obligation); uint256 newNetCredit = IMidnight(midnight).creditOf(obligationId, address(this)) - IMidnight(midnight).pendingFee(obligationId, address(this)); // new net credit cannot be > old credit @@ -304,7 +305,7 @@ contract MidnightAdapter is IMidnightAdapter { int256 change = newNetCredit.toInt256() - netCredit[obligationId].toInt256(); accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); // change is at most buyNetCreditIncrease if (change < buyNetCreditIncrease.toInt256()) { @@ -314,9 +315,9 @@ contract MidnightAdapter is IMidnightAdapter { if (maturityData.netCredit == 0 && buyNetCreditIncrease > 0 && maturity > block.timestamp) { activableMaturities--; - uint256 group = maturity.group(); + uint256 index = maturity.bitmapIndex(); uint256 bit = (maturity / 1 hours) % 256; - bitmaps[group] = bitmaps[group] | (uint256(1) << bit); + bitmaps[index] = bitmaps[index] | (uint256(1) << bit); } IVaultV2(parentVault).allocate(address(this), abi.encode(ids(obligation), change), paidAssets); @@ -355,7 +356,7 @@ contract MidnightAdapter is IMidnightAdapter { require(seller == address(this), NotSelf()); accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); if (totalNetCreditDecrease > 0) { removeUnits(obligationId, obligation.maturity.align(), totalNetCreditDecrease); @@ -395,19 +396,19 @@ contract MidnightAdapter is IMidnightAdapter { if (removedUnits > 0 && maturityData.netCredit == 0 && maturity > block.timestamp) { activableMaturities++; - uint256 group = maturity.group(); + uint256 index = maturity.bitmapIndex(); uint256 bit = (maturity / 1 hours) % 256; - bitmaps[group] = bitmaps[group] & ~(uint256(1) << bit); + bitmaps[index] = bitmaps[index] & ~(uint256(1) << bit); } } - function durationIndex(uint256 maturity) internal view returns (uint256 index) { + function durationCount(uint256 maturity) internal view returns (uint256 count) { uint256 timeToMaturity = maturity.zeroFloorSub(block.timestamp); - while (index < durationsLength && timeToMaturity >= packedDurations.get(index)) index++; + while (count < durationsLength && timeToMaturity >= packedDurations.get(count)) count++; } function ids(Obligation memory obligation) public view returns (bytes32[] memory) { - uint256 durationsCount = durationIndex(obligation.maturity.align()); + uint256 durationsCount = durationCount(obligation.maturity.align()); bytes32[] memory idsArray = new bytes32[](1 + obligation.collateralParams.length * 2 + durationsCount); diff --git a/src/adapters/interfaces/IMidnightAdapter.sol b/src/adapters/interfaces/IMidnightAdapter.sol index 612b26c6e..f1761d902 100644 --- a/src/adapters/interfaces/IMidnightAdapter.sol +++ b/src/adapters/interfaces/IMidnightAdapter.sol @@ -10,7 +10,7 @@ import {IRatifier} from "lib/midnight/src/interfaces/IRatifier.sol"; struct MaturityData { uint128 netCredit; uint128 growth; - uint8 durationIndex; + uint8 durationCount; } interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier { @@ -57,7 +57,7 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier { function skim(address token) external; function durations() external view returns (uint256[] memory); function durationsLength() external view returns (uint256); - function updateDurationIndexAndAllocations(Obligation memory obligation) external; + function updateDurationCountAndAllocations(Obligation memory obligation) external; function withdrawToVault(Obligation memory obligation, uint256 units) external; function ids(Obligation memory obligation) external view returns (bytes32[] memory); function parentVault() external view returns (address); diff --git a/src/adapters/libraries/MaturitiesLib.sol b/src/adapters/libraries/MaturitiesLib.sol index 1affde130..b57e48f31 100644 --- a/src/adapters/libraries/MaturitiesLib.sol +++ b/src/adapters/libraries/MaturitiesLib.sol @@ -9,15 +9,15 @@ library MaturitiesLib { return (_maturity + h - 1) / h * h; } - /// @dev Bitmap group containing the already aligned maturity. - function group(uint256 alignedMaturity) internal pure returns (uint256) { + /// @dev Index of the bitmap containing the already aligned maturity. + function bitmapIndex(uint256 alignedMaturity) internal pure returns (uint256) { return alignedMaturity / (256 * 1 hours); } - /// @dev Starting maturity of a bitmap group. + /// @dev Earliest maturity in the bitmap at index. /// @dev Always aligned to an hour. - function maturity(uint256 _group) internal pure returns (uint256) { - return _group * 256 * 1 hours; + function maturity(uint256 index) internal pure returns (uint256) { + return index * 256 * 1 hours; } /// @dev Least significant set bit. Assumes `bitmap` is not zero. diff --git a/test/MidnightAdapterAllocationUpdateTest.sol b/test/MidnightAdapterAllocationUpdateTest.sol index 19f6a99cf..425a2aba8 100644 --- a/test/MidnightAdapterAllocationUpdateTest.sol +++ b/test/MidnightAdapterAllocationUpdateTest.sol @@ -114,16 +114,16 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { return keccak256(abi.encode("duration", duration)); } - function testExactDuration(uint32 durationIndex) public { - durationIndex = uint32(bound(durationIndex, 0, adapter.durationsLength() - 1)); - uint256 duration = adapter.durations()[durationIndex]; + function testExactDuration(uint32 durationCount) public { + durationCount = uint32(bound(durationCount, 0, adapter.durationsLength() - 1)); + uint256 duration = adapter.durations()[durationCount]; buy(duration, 1e18); assertEq(parentVault.allocation(durationId(duration)), 1e18); } - function testExitDuration(uint256 durationIndex, uint256 timeToMaturity, uint256 extraSkip) public { - durationIndex = bound(durationIndex, 0, adapter.durationsLength() - 1); - uint256 duration = adapter.durations()[durationIndex]; + function testExitDuration(uint256 durationCount, uint256 timeToMaturity, uint256 extraSkip) public { + durationCount = bound(durationCount, 0, adapter.durationsLength() - 1); + uint256 duration = adapter.durations()[durationCount]; timeToMaturity = bound(timeToMaturity, duration, adapter.maxTtmWhenBuying() - 1 hours); extraSkip = bound(extraSkip, 1 hours + 1, 10 * 365 days); @@ -132,24 +132,24 @@ contract MidnightAdapterAllocationUpdateTest is MidnightAdapterTest { skip(timeToMaturity - duration + extraSkip); - adapter.updateDurationIndexAndAllocations(offer.obligation); + adapter.updateDurationCountAndAllocations(offer.obligation); assertEq(parentVault.allocation(durationId(duration)), 0); } - function testRepeatDeallocateExpiredDurations(uint256 durationIndex, uint256 timeToMaturity, uint256 skipAmount) + function testRepeatDeallocateExpiredDurations(uint256 durationCount, uint256 timeToMaturity, uint256 skipAmount) public { - durationIndex = bound(durationIndex, 0, adapter.durationsLength() - 1); - uint256 duration = adapter.durations()[durationIndex]; + durationCount = bound(durationCount, 0, adapter.durationsLength() - 1); + uint256 duration = adapter.durations()[durationCount]; timeToMaturity = bound(timeToMaturity, duration, adapter.maxTtmWhenBuying() - 1 hours); skipAmount = bound(skipAmount, 0, duration * 2); Offer memory offer = buy(timeToMaturity, 1e18); skip(skipAmount); - adapter.updateDurationIndexAndAllocations(offer.obligation); + adapter.updateDurationCountAndAllocations(offer.obligation); uint256 savedAllocation = parentVault.allocation(durationId(duration)); - adapter.updateDurationIndexAndAllocations(offer.obligation); + adapter.updateDurationCountAndAllocations(offer.obligation); assertEq(parentVault.allocation(durationId(duration)), savedAllocation); } From 34fade0ae1057bf455dac306aa185039d9666d87 Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 15:52:51 +0200 Subject: [PATCH 4/6] renames --- src/adapters/MidnightAdapter.sol | 28 ++++++++++---------- src/adapters/interfaces/IMidnightAdapter.sol | 6 ++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 5769a2a70..3d1d89349 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -113,7 +113,7 @@ contract MidnightAdapter is IMidnightAdapter { uint256 totalNetCreditDecrease = netCredit[obligationId] - newNetCredit; accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); if (totalNetCreditDecrease > 0) removeUnits(obligationId, obligation.maturity, totalNetCreditDecrease); @@ -121,16 +121,16 @@ contract MidnightAdapter is IMidnightAdapter { IVaultV2(parentVault).deallocate(address(this), abi.encode(ids(obligation), change), withdrawnAssets); } - function updateDurationIndexAndAllocations(Obligation memory obligation) public { + function updateDurationCountAndAllocations(Obligation memory obligation) public { MaturityData storage maturityData = _maturities[obligation.maturity]; - uint256 oldDurationIndex = maturityData.durationIndex; - uint256 newDurationIndex = durationIndex(obligation.maturity); - maturityData.durationIndex = uint8(newDurationIndex); + uint256 oldDurationCount = maturityData.durationCount; + uint256 newDurationCount = durationCount(obligation.maturity); + maturityData.durationCount = uint8(newDurationCount); // VaultV2.deallocate requires allocation > 0 for each returned id. - if (newDurationIndex < oldDurationIndex && maturityData.netCredit > 0) { - bytes32[] memory zeroedDurationsIds = new bytes32[](oldDurationIndex - newDurationIndex); + if (newDurationCount < oldDurationCount && maturityData.netCredit > 0) { + bytes32[] memory zeroedDurationsIds = new bytes32[](oldDurationCount - newDurationCount); for (uint256 i = 0; i < zeroedDurationsIds.length; i++) { - zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newDurationIndex + i))); + zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newDurationCount + i))); } int256 change = -int256(uint256(maturityData.netCredit)); IVaultV2(parentVault).deallocate(address(this), abi.encode(zeroedDurationsIds, change), 0); @@ -209,7 +209,7 @@ contract MidnightAdapter is IMidnightAdapter { require(IMidnight(midnight).debtOf(obligationId, address(this)) == 0, NoBorrowing()); accrueInterest(); - updateDurationIndexAndAllocations(offer.obligation); + updateDurationCountAndAllocations(offer.obligation); uint256 newNetCredit = IMidnight(midnight).creditOf(obligationId, address(this)) - IMidnight(midnight).pendingFee(obligationId, address(this)); // new net credit cannot be > old credit @@ -280,7 +280,7 @@ contract MidnightAdapter is IMidnightAdapter { int256 change = newNetCredit.toInt256() - netCredit[obligationId].toInt256(); accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); // change is at most buyNetCreditIncrease if (change < buyNetCreditIncrease.toInt256()) { @@ -355,7 +355,7 @@ contract MidnightAdapter is IMidnightAdapter { require(seller == address(this), NotSelf()); accrueInterest(); - updateDurationIndexAndAllocations(obligation); + updateDurationCountAndAllocations(obligation); if (totalNetCreditDecrease > 0) removeUnits(obligationId, obligation.maturity, totalNetCreditDecrease); @@ -408,13 +408,13 @@ contract MidnightAdapter is IMidnightAdapter { } } - function durationIndex(uint256 maturity) internal view returns (uint256 index) { + function durationCount(uint256 maturity) internal view returns (uint256 count) { uint256 timeToMaturity = maturity.zeroFloorSub(block.timestamp); - while (index < durationsLength && timeToMaturity >= packedDurations.get(index)) index++; + while (count < durationsLength && timeToMaturity >= packedDurations.get(count)) count++; } function ids(Obligation memory obligation) public view returns (bytes32[] memory) { - uint256 durationsCount = durationIndex(obligation.maturity); + uint256 durationsCount = durationCount(obligation.maturity); bytes32[] memory idsArray = new bytes32[](1 + obligation.collateralParams.length * 2 + durationsCount); diff --git a/src/adapters/interfaces/IMidnightAdapter.sol b/src/adapters/interfaces/IMidnightAdapter.sol index ad85c21e9..60ae57661 100644 --- a/src/adapters/interfaces/IMidnightAdapter.sol +++ b/src/adapters/interfaces/IMidnightAdapter.sol @@ -14,7 +14,7 @@ struct MaturityData { uint128 growth; uint48 prevMaturity; uint48 nextMaturity; - uint8 durationIndex; + uint8 durationCount; } interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier { @@ -50,7 +50,7 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier { function lastUpdate() external view returns (uint48); function firstMaturity() external view returns (uint48); function currentGrowth() external view returns (uint128); - function activeMaturities() external view returns (uint256); + function activableMaturities() external view returns (uint256); function midnight() external view returns (address); function adapterId() external view returns (bytes32); function packedDurations() external view returns (bytes32); @@ -61,7 +61,7 @@ interface IMidnightAdapter is IAdapter, ICallbacks, IRatifier { function skim(address token) external; function durations() external view returns (uint256[] memory); function durationsLength() external view returns (uint256); - function updateDurationIndexAndAllocations(Obligation memory obligation) external; + function updateDurationCountAndAllocations(Obligation memory obligation) external; function withdrawToVault(Obligation memory obligation, uint256 units) external; function ids(Obligation memory obligation) external view returns (bytes32[] memory); function parentVault() external view returns (address); From 8e69657408adc06b0243adb34cd9f349edf851cf Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 16:21:19 +0200 Subject: [PATCH 5/6] typos --- src/adapters/MidnightAdapter.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 37c5c4fae..0dd7a1c4d 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -131,13 +131,13 @@ contract MidnightAdapter is IMidnightAdapter { uint256 maturity = obligation.maturity.align(); MaturityData storage maturityData = maturitiesData[maturity]; uint256 olddurationCount = maturityData.durationCount; - uint256 newdurationCount = durationCount(maturity); - maturityData.durationCount = uint8(newdurationCount); + uint256 newDurationCount = durationCount(maturity); + maturityData.durationCount = uint8(newDurationCount); // VaultV2.deallocate requires allocation > 0 for each returned id. - if (newdurationCount < olddurationCount && maturityData.netCredit > 0) { - bytes32[] memory zeroedDurationsIds = new bytes32[](olddurationCount - newdurationCount); + if (newDurationCount < olddurationCount && maturityData.netCredit > 0) { + bytes32[] memory zeroedDurationsIds = new bytes32[](olddurationCount - newDurationCount); for (uint256 i = 0; i < zeroedDurationsIds.length; i++) { - zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newdurationCount + i))); + zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newDurationCount + i))); } int256 change = -int256(uint256(maturityData.netCredit)); IVaultV2(parentVault).deallocate(address(this), abi.encode(zeroedDurationsIds, change), 0); From 80f6c3b936f6a1321a38e10b98b03998662cac57 Mon Sep 17 00:00:00 2001 From: Adrien Husson Date: Mon, 20 Apr 2026 16:21:59 +0200 Subject: [PATCH 6/6] typos --- src/adapters/MidnightAdapter.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 0dd7a1c4d..5d8cb604c 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -130,12 +130,12 @@ contract MidnightAdapter is IMidnightAdapter { function updateDurationCountAndAllocations(Obligation memory obligation) public { uint256 maturity = obligation.maturity.align(); MaturityData storage maturityData = maturitiesData[maturity]; - uint256 olddurationCount = maturityData.durationCount; + uint256 oldDurationCount = maturityData.durationCount; uint256 newDurationCount = durationCount(maturity); maturityData.durationCount = uint8(newDurationCount); // VaultV2.deallocate requires allocation > 0 for each returned id. - if (newDurationCount < olddurationCount && maturityData.netCredit > 0) { - bytes32[] memory zeroedDurationsIds = new bytes32[](olddurationCount - newDurationCount); + if (newDurationCount < oldDurationCount && maturityData.netCredit > 0) { + bytes32[] memory zeroedDurationsIds = new bytes32[](oldDurationCount - newDurationCount); for (uint256 i = 0; i < zeroedDurationsIds.length; i++) { zeroedDurationsIds[i] = keccak256(abi.encode("duration", packedDurations.get(newDurationCount + i))); }