From 49c532a588c61d83289f82ee643636d0e75553d4 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:30:41 +0000 Subject: [PATCH 1/4] fix: enforce non-decreasing settlement fees Reject fee curves that decrease as TTM increases and update tests to configure monotonic curves. --- src/Midnight.sol | 24 +++++++- src/interfaces/IMidnight.sol | 1 + test/MidnightBundlesTest.sol | 3 +- test/OtherFunctionsTest.sol | 6 +- test/SettersTest.sol | 108 +++++++++++++++++++++++++---------- test/SettlementFeeTest.sol | 23 +++++--- test/TakeAmountsTest.sol | 7 ++- test/TakeTest.sol | 10 +++- 8 files changed, 136 insertions(+), 46 deletions(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index 32c8c71f7..798f0acba 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -42,6 +42,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev The settlement fee is a piecewise linear function on the TTM (time to maturity). It is computed with linear /// approximation between breakpoints. /// @dev Settlement fee breakpoint indices: 0=0d, 1=1d, 2=7d, 3=30d, 4=90d, 5=180d, 6=360d. +/// @dev Settlement fees are non-decreasing with the TTM. /// @dev For TTM > 360d, the settlement fee is the fee at the 360d breakpoint. /// @dev Post-maturity, the settlement fee is the fee at the 0d breakpoint. /// @dev Settlement fees are stored in cbp (centi-basis-points): settlementFee / CBP. @@ -264,6 +265,17 @@ contract Midnight is IMidnight { require(_marketState.tickSpacing > 0, MarketNotCreated()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); + uint16[7] memory feeCbps = [ + _marketState.settlementFeeCbp0, + _marketState.settlementFeeCbp1, + _marketState.settlementFeeCbp2, + _marketState.settlementFeeCbp3, + _marketState.settlementFeeCbp4, + _marketState.settlementFeeCbp5, + _marketState.settlementFeeCbp6 + ]; + feeCbps[index] = newSettlementFeeCbp; + _requireSettlementFeeCbpsNonDecreasing(feeCbps); if (index == 0) _marketState.settlementFeeCbp0 = newSettlementFeeCbp; else if (index == 1) _marketState.settlementFeeCbp1 = newSettlementFeeCbp; else if (index == 2) _marketState.settlementFeeCbp2 = newSettlementFeeCbp; @@ -280,10 +292,20 @@ contract Midnight is IMidnight { require(newSettlementFee <= maxSettlementFee(index), SettlementFeeTooHigh()); require(newSettlementFee % CBP == 0, FeeNotMultipleOfFeeCbp()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP - defaultSettlementFeeCbp[loanToken][index] = uint16(newSettlementFee / CBP); + uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); + uint16[7] memory feeCbps = defaultSettlementFeeCbp[loanToken]; + feeCbps[index] = newSettlementFeeCbp; + _requireSettlementFeeCbpsNonDecreasing(feeCbps); + defaultSettlementFeeCbp[loanToken][index] = newSettlementFeeCbp; emit EventsLib.SetDefaultSettlementFee(loanToken, index, newSettlementFee); } + function _requireSettlementFeeCbpsNonDecreasing(uint16[7] memory feeCbps) internal pure { + for (uint256 i = 1; i < 7; ++i) { + require(feeCbps[i - 1] <= feeCbps[i], SettlementFeeNotMonotonic()); + } + } + function setMarketContinuousFee(bytes32 id, uint256 newContinuousFee) external { MarketState storage _marketState = marketState[id]; require(msg.sender == feeSetter, OnlyFeeSetter()); diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 129db5854..35a7b0000 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -96,6 +96,7 @@ interface IMidnight { error OfferNotStarted(); error OnlyFeeClaimer(); error OnlyFeeSetter(); + error SettlementFeeNotMonotonic(); error OnlyRoleSetter(); error OnlyTickSpacingSetter(); error RatifierFail(); diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index c480c4515..95840c2d2 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -41,8 +41,9 @@ contract MidnightBundlesTest is BaseTest { // Set settlement fees to max for all breakpoints. midnight.setFeeClaimer(makeAddr("feeClaimer")); - for (uint256 i; i <= 6; i++) { + for (uint256 i = 6;; --i) { midnight.setDefaultSettlementFee(address(loanToken), i, maxSettlementFee(i)); + if (i == 0) break; } market.loanToken = address(loanToken); diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 8c79f4d45..09e3020c6 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -233,8 +233,9 @@ contract OtherFunctionsTest is BaseTest { _market = validMarket(_market); midnight.setDefaultContinuousFee(_market.loanToken, MAX_CONTINUOUS_FEE); - for (uint256 i = 0; i < 7; i++) { + for (uint256 i = 6;; --i) { midnight.setDefaultSettlementFee(_market.loanToken, i, maxSettlementFee(i)); + if (i == 0) break; } bytes32 _id = midnight.touchMarket(_market); @@ -637,8 +638,9 @@ contract OtherFunctionsTest is BaseTest { _defaultContinuousFee = bound(_defaultContinuousFee, 0, MAX_CONTINUOUS_FEE); midnight.setDefaultContinuousFee(_market.loanToken, _defaultContinuousFee); - for (uint256 i = 0; i < 7; i++) { + for (uint256 i = 6;; --i) { midnight.setDefaultSettlementFee(_market.loanToken, i, maxSettlementFee(i)); + if (i == 0) break; } bytes32 _id = midnight.touchMarket(_market); diff --git a/test/SettersTest.sol b/test/SettersTest.sol index d3ec3bd15..d3f31340c 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -16,6 +16,23 @@ import {BaseTest} from "./BaseTest.sol"; import {IMidnight, Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; contract SettersTest is BaseTest { + function _createMarket(address loanToken_) internal returns (bytes32) { + CollateralParams[] memory collateralParams = new CollateralParams[](1); + collateralParams[0] = CollateralParams({ + token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + }); + Market memory market = Market({ + loanToken: loanToken_, + maturity: vm.getBlockTimestamp() + 1 days, + collateralParams: collateralParams, + rcfThreshold: 0, + enterGate: address(0), + liquidatorGate: address(0) + }); + midnight.touchMarket(market); + return toId(market); + } + function testMaxSettlementFeeConstants() public pure { assertEq(maxSettlementFee(0), MAX_SETTLEMENT_FEE_0_DAYS, "0 days max settlement fee"); assertEq(maxSettlementFee(1), MAX_SETTLEMENT_FEE_1_DAY, "1 day max settlement fee"); @@ -65,12 +82,12 @@ contract SettersTest is BaseTest { uint256 threeSixtyDaysFee ) public { postMaturityFee = bound(postMaturityFee, 0, maxSettlementFee(0)) / 1e12 * 1e12; - oneDayFee = bound(oneDayFee, 0, maxSettlementFee(1)) / 1e12 * 1e12; - sevenDaysFee = bound(sevenDaysFee, 0, maxSettlementFee(2)) / 1e12 * 1e12; - thirtyDaysFee = bound(thirtyDaysFee, 0, maxSettlementFee(3)) / 1e12 * 1e12; - ninetyDaysFee = bound(ninetyDaysFee, 0, maxSettlementFee(4)) / 1e12 * 1e12; - oneEightyDaysFee = bound(oneEightyDaysFee, 0, maxSettlementFee(5)) / 1e12 * 1e12; - threeSixtyDaysFee = bound(threeSixtyDaysFee, 0, maxSettlementFee(6)) / 1e12 * 1e12; + oneDayFee = bound(oneDayFee, postMaturityFee, maxSettlementFee(1)) / 1e12 * 1e12; + sevenDaysFee = bound(sevenDaysFee, oneDayFee, maxSettlementFee(2)) / 1e12 * 1e12; + thirtyDaysFee = bound(thirtyDaysFee, sevenDaysFee, maxSettlementFee(3)) / 1e12 * 1e12; + ninetyDaysFee = bound(ninetyDaysFee, thirtyDaysFee, maxSettlementFee(4)) / 1e12 * 1e12; + oneEightyDaysFee = bound(oneEightyDaysFee, ninetyDaysFee, maxSettlementFee(5)) / 1e12 * 1e12; + threeSixtyDaysFee = bound(threeSixtyDaysFee, oneEightyDaysFee, maxSettlementFee(6)) / 1e12 * 1e12; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -87,13 +104,13 @@ contract SettersTest is BaseTest { bytes32 id = toId(market); midnight.touchMarket(market); - midnight.setMarketSettlementFee(id, 0, postMaturityFee); - midnight.setMarketSettlementFee(id, 1, oneDayFee); - midnight.setMarketSettlementFee(id, 2, sevenDaysFee); - midnight.setMarketSettlementFee(id, 3, thirtyDaysFee); - midnight.setMarketSettlementFee(id, 4, ninetyDaysFee); - midnight.setMarketSettlementFee(id, 5, oneEightyDaysFee); midnight.setMarketSettlementFee(id, 6, threeSixtyDaysFee); + midnight.setMarketSettlementFee(id, 5, oneEightyDaysFee); + midnight.setMarketSettlementFee(id, 4, ninetyDaysFee); + midnight.setMarketSettlementFee(id, 3, thirtyDaysFee); + midnight.setMarketSettlementFee(id, 2, sevenDaysFee); + midnight.setMarketSettlementFee(id, 1, oneDayFee); + midnight.setMarketSettlementFee(id, 0, postMaturityFee); assertEq(midnight.settlementFee(id, 0), postMaturityFee, "post maturity settlement fee"); assertEq(midnight.settlementFee(id, 1 days), oneDayFee, "one day settlement fee"); @@ -139,6 +156,37 @@ contract SettersTest is BaseTest { midnight.setDefaultSettlementFee(loanToken, index, fee); } + function testSetMarketSettlementFeeRejectsDecreaseWithLongerTtm() public { + bytes32 id = _createMarket(address(loanToken)); + + vm.expectRevert(IMidnight.SettlementFeeNotMonotonic.selector); + midnight.setMarketSettlementFee(id, 1, 1e12); + } + + function testSetMarketSettlementFeeRejectsDecreaseWithShorterTtm() public { + bytes32 id = _createMarket(address(loanToken)); + midnight.setMarketSettlementFee(id, 6, 3e12); + midnight.setMarketSettlementFee(id, 5, 2e12); + midnight.setMarketSettlementFee(id, 4, 1e12); + + vm.expectRevert(IMidnight.SettlementFeeNotMonotonic.selector); + midnight.setMarketSettlementFee(id, 5, 0); + } + + function testSetDefaultSettlementFeeRejectsDecreaseWithLongerTtm() public { + vm.expectRevert(IMidnight.SettlementFeeNotMonotonic.selector); + midnight.setDefaultSettlementFee(address(loanToken), 1, 1e12); + } + + function testSetDefaultSettlementFeeRejectsDecreaseWithShorterTtm() public { + midnight.setDefaultSettlementFee(address(loanToken), 6, 3e12); + midnight.setDefaultSettlementFee(address(loanToken), 5, 2e12); + midnight.setDefaultSettlementFee(address(loanToken), 4, 1e12); + + vm.expectRevert(IMidnight.SettlementFeeNotMonotonic.selector); + midnight.setDefaultSettlementFee(address(loanToken), 5, 0); + } + function testSetMarketSettlementFeeMarketNotCreated(bytes32 id) public { vm.expectRevert(IMidnight.MarketNotCreated.selector); midnight.setMarketSettlementFee(id, 0, 0); @@ -194,13 +242,13 @@ contract SettersTest is BaseTest { oneEightyDaysFee = bound(oneEightyDaysFee, ninetyDaysFee, maxSettlementFee(5)) / 1e12 * 1e12; threeSixtyDaysFee = bound(threeSixtyDaysFee, oneEightyDaysFee, maxSettlementFee(6)) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(loanToken, 0, postMaturityFee); - midnight.setDefaultSettlementFee(loanToken, 1, oneDayFee); - midnight.setDefaultSettlementFee(loanToken, 2, sevenDaysFee); - midnight.setDefaultSettlementFee(loanToken, 3, thirtyDaysFee); - midnight.setDefaultSettlementFee(loanToken, 4, ninetyDaysFee); - midnight.setDefaultSettlementFee(loanToken, 5, oneEightyDaysFee); midnight.setDefaultSettlementFee(loanToken, 6, threeSixtyDaysFee); + midnight.setDefaultSettlementFee(loanToken, 5, oneEightyDaysFee); + midnight.setDefaultSettlementFee(loanToken, 4, ninetyDaysFee); + midnight.setDefaultSettlementFee(loanToken, 3, thirtyDaysFee); + midnight.setDefaultSettlementFee(loanToken, 2, sevenDaysFee); + midnight.setDefaultSettlementFee(loanToken, 1, oneDayFee); + midnight.setDefaultSettlementFee(loanToken, 0, postMaturityFee); // touch market with this loan token CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -253,12 +301,12 @@ contract SettersTest is BaseTest { uint256 settlementFee6 ) public { settlementFee0 = bound(settlementFee0, 0, maxSettlementFee(0)) / 1e12 * 1e12; - settlementFee1 = bound(settlementFee1, 0, maxSettlementFee(1)) / 1e12 * 1e12; - settlementFee2 = bound(settlementFee2, 0, maxSettlementFee(2)) / 1e12 * 1e12; - settlementFee3 = bound(settlementFee3, 0, maxSettlementFee(3)) / 1e12 * 1e12; - settlementFee4 = bound(settlementFee4, 0, maxSettlementFee(4)) / 1e12 * 1e12; - settlementFee5 = bound(settlementFee5, 0, maxSettlementFee(5)) / 1e12 * 1e12; - settlementFee6 = bound(settlementFee6, 0, maxSettlementFee(6)) / 1e12 * 1e12; + settlementFee1 = bound(settlementFee1, settlementFee0, maxSettlementFee(1)) / 1e12 * 1e12; + settlementFee2 = bound(settlementFee2, settlementFee1, maxSettlementFee(2)) / 1e12 * 1e12; + settlementFee3 = bound(settlementFee3, settlementFee2, maxSettlementFee(3)) / 1e12 * 1e12; + settlementFee4 = bound(settlementFee4, settlementFee3, maxSettlementFee(4)) / 1e12 * 1e12; + settlementFee5 = bound(settlementFee5, settlementFee4, maxSettlementFee(5)) / 1e12 * 1e12; + settlementFee6 = bound(settlementFee6, settlementFee5, maxSettlementFee(6)) / 1e12 * 1e12; CollateralParams[] memory cols = new CollateralParams[](1); cols[0] = CollateralParams({ @@ -275,13 +323,13 @@ contract SettersTest is BaseTest { bytes32 id = toId(market); midnight.touchMarket(market); - midnight.setMarketSettlementFee(id, 0, settlementFee0); - midnight.setMarketSettlementFee(id, 1, settlementFee1); - midnight.setMarketSettlementFee(id, 2, settlementFee2); - midnight.setMarketSettlementFee(id, 3, settlementFee3); - midnight.setMarketSettlementFee(id, 4, settlementFee4); - midnight.setMarketSettlementFee(id, 5, settlementFee5); midnight.setMarketSettlementFee(id, 6, settlementFee6); + midnight.setMarketSettlementFee(id, 5, settlementFee5); + midnight.setMarketSettlementFee(id, 4, settlementFee4); + midnight.setMarketSettlementFee(id, 3, settlementFee3); + midnight.setMarketSettlementFee(id, 2, settlementFee2); + midnight.setMarketSettlementFee(id, 1, settlementFee1); + midnight.setMarketSettlementFee(id, 0, settlementFee0); // Test exact breakpoints assertEq(midnight.settlementFee(id, 0), settlementFee0, "0 days"); diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index 0096abeed..0622afd93 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -31,6 +31,13 @@ contract SettlementFeeTest is BaseTest { Offer internal borrowerOffer; address internal feeClaimer = makeAddr("feeClaimer"); + function _setDefaultSettlementFeeFrom(uint256 index, uint256 settlementFee) internal { + for (uint256 i = 6;; --i) { + midnight.setDefaultSettlementFee(address(loanToken), i, settlementFee); + if (i == index) break; + } + } + function setUp() public override { super.setUp(); @@ -87,7 +94,7 @@ contract SettlementFeeTest is BaseTest { uint256 sellerPrice = TickLib.tickToPrice(sellerTick); vm.assume(sellerPrice >= MIN_SELLER_PRICE); settlementFee = bound(settlementFee, 0, maxSettlementFee(1)) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); midnight.touchMarket(market); midnight.setMarketTickSpacing(id, 1); borrowerOffer.tick = sellerTick; @@ -112,7 +119,7 @@ contract SettlementFeeTest is BaseTest { uint256 buyerPrice = TickLib.tickToPrice(buyerTick); vm.assume(buyerPrice >= MIN_SELLER_PRICE); settlementFee = bound(settlementFee, 0, maxSettlementFee(1)) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); lenderOffer.tick = buyerTick; uint256 sellerPrice = buyerPrice - settlementFee; @@ -134,7 +141,7 @@ contract SettlementFeeTest is BaseTest { uint256 sellerPrice = TickLib.tickToPrice(sellerTick); vm.assume(sellerPrice >= MIN_SELLER_PRICE); settlementFee = bound(settlementFee, 0, maxSettlementFee(1)) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); borrowerOffer.tick = sellerTick; uint256 buyerPrice = sellerPrice + settlementFee; @@ -168,8 +175,8 @@ contract SettlementFeeTest is BaseTest { // Set fees at breakpoints for linear interpolation (3 days is between 1 and 7 days) // Must be set before touchMarket, which snapshots defaultFees at creation time. + _setDefaultSettlementFeeFrom(2, settlementFee7Days); midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee1Day); - midnight.setDefaultSettlementFee(address(loanToken), 2, settlementFee7Days); id = midnight.touchMarket(market); lenderOffer.market = market; @@ -209,7 +216,7 @@ contract SettlementFeeTest is BaseTest { lenderOffer.market = market; borrowerOffer.market = market; - midnight.setDefaultSettlementFee(address(loanToken), 0, settlementFee0Day); + _setDefaultSettlementFeeFrom(0, settlementFee0Day); borrowerOffer.tick = sellerTick; collateralize(market, borrower, MAX_DEBT); @@ -255,7 +262,7 @@ contract SettlementFeeTest is BaseTest { function testClaimSettlementFee(uint256 settlementFee, uint256 units, uint256 withdrawAmount) public { units = bound(units, 1, MAX_DEBT); settlementFee = bound(settlementFee, 1e12, maxSettlementFee(1)) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); collateralize(market, borrower, MAX_DEBT); take(units, lender, borrowerOffer); @@ -281,7 +288,7 @@ contract SettlementFeeTest is BaseTest { function testClaimSettlementFeeExcessReverts() public { uint256 settlementFee = maxSettlementFee(1) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); borrowerOffer.tick = 0; collateralize(market, borrower, MAX_DEBT); @@ -296,7 +303,7 @@ contract SettlementFeeTest is BaseTest { function testSettlementFeesAccumulate() public { uint256 settlementFee = maxSettlementFee(1) / 1e12 * 1e12; - midnight.setDefaultSettlementFee(address(loanToken), 1, settlementFee); + _setDefaultSettlementFeeFrom(1, settlementFee); borrowerOffer.tick = 0; borrowerOffer.group = keccak256("g1"); diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index 22886ee40..23350b55b 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -59,10 +59,13 @@ contract TakeAmountsTest is BaseTest { returns (uint256 settlementFee) { settlementFee0 = bound(settlementFee0, 0, maxSettlementFee(0)) / 1e12 * 1e12; - settlementFee1 = bound(settlementFee1, 0, maxSettlementFee(1)) / 1e12 * 1e12; + settlementFee1 = bound(settlementFee1, settlementFee0, maxSettlementFee(1)) / 1e12 * 1e12; midnight.touchMarket(market); + for (uint256 i = 6;; --i) { + midnight.setMarketSettlementFee(id, i, settlementFee1); + if (i == 1) break; + } midnight.setMarketSettlementFee(id, 0, settlementFee0); - midnight.setMarketSettlementFee(id, 1, settlementFee1); settlementFee = midnight.settlementFee(id, market.maturity - vm.getBlockTimestamp()); } diff --git a/test/TakeTest.sol b/test/TakeTest.sol index bb331b0da..d6f520442 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -1223,7 +1223,10 @@ contract TakeTest is BaseTest { // fee>0, buy, units function testPriceZeroWithSettlementFeeBuy() public { midnight.touchMarket(market); - midnight.setMarketSettlementFee(id, 1, 1e12); + for (uint256 i = 6;; --i) { + midnight.setMarketSettlementFee(id, i, 1e12); + if (i == 1) break; + } uint256 units = 1e18; lenderOffer.tick = 0; lenderOffer.maxUnits = units; @@ -1235,7 +1238,10 @@ contract TakeTest is BaseTest { // fee>0, sell, units function testPriceZeroWithSettlementFeeSell() public { midnight.touchMarket(market); - midnight.setMarketSettlementFee(id, 1, 1e12); + for (uint256 i = 6;; --i) { + midnight.setMarketSettlementFee(id, i, 1e12); + if (i == 1) break; + } uint256 fee = midnight.settlementFee(id, market.maturity - vm.getBlockTimestamp()); uint256 units = 1e18; borrowerOffer.tick = 0; From 355e70d48ce613a44c967082941cb765af9192df Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:54:39 +0000 Subject: [PATCH 2/4] refactor: check adjacent fee breakpoints Only compare the updated settlement-fee breakpoint with its immediate neighbors. --- src/Midnight.sol | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index 798f0acba..0d393de4e 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -265,17 +265,12 @@ contract Midnight is IMidnight { require(_marketState.tickSpacing > 0, MarketNotCreated()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); - uint16[7] memory feeCbps = [ - _marketState.settlementFeeCbp0, - _marketState.settlementFeeCbp1, - _marketState.settlementFeeCbp2, - _marketState.settlementFeeCbp3, - _marketState.settlementFeeCbp4, - _marketState.settlementFeeCbp5, - _marketState.settlementFeeCbp6 - ]; - feeCbps[index] = newSettlementFeeCbp; - _requireSettlementFeeCbpsNonDecreasing(feeCbps); + _requireSettlementFeeCbpNonDecreasing( + index, + newSettlementFeeCbp, + index == 0 ? 0 : _settlementFeeCbpAt(_marketState, index - 1), + index == 6 ? 0 : _settlementFeeCbpAt(_marketState, index + 1) + ); if (index == 0) _marketState.settlementFeeCbp0 = newSettlementFeeCbp; else if (index == 1) _marketState.settlementFeeCbp1 = newSettlementFeeCbp; else if (index == 2) _marketState.settlementFeeCbp2 = newSettlementFeeCbp; @@ -293,17 +288,32 @@ contract Midnight is IMidnight { require(newSettlementFee % CBP == 0, FeeNotMultipleOfFeeCbp()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); - uint16[7] memory feeCbps = defaultSettlementFeeCbp[loanToken]; + uint16[7] storage feeCbps = defaultSettlementFeeCbp[loanToken]; + _requireSettlementFeeCbpNonDecreasing( + index, newSettlementFeeCbp, index == 0 ? 0 : feeCbps[index - 1], index == 6 ? 0 : feeCbps[index + 1] + ); feeCbps[index] = newSettlementFeeCbp; - _requireSettlementFeeCbpsNonDecreasing(feeCbps); - defaultSettlementFeeCbp[loanToken][index] = newSettlementFeeCbp; emit EventsLib.SetDefaultSettlementFee(loanToken, index, newSettlementFee); } - function _requireSettlementFeeCbpsNonDecreasing(uint16[7] memory feeCbps) internal pure { - for (uint256 i = 1; i < 7; ++i) { - require(feeCbps[i - 1] <= feeCbps[i], SettlementFeeNotMonotonic()); - } + function _settlementFeeCbpAt(MarketState storage _marketState, uint256 index) internal view returns (uint16) { + if (index == 0) return _marketState.settlementFeeCbp0; + if (index == 1) return _marketState.settlementFeeCbp1; + if (index == 2) return _marketState.settlementFeeCbp2; + if (index == 3) return _marketState.settlementFeeCbp3; + if (index == 4) return _marketState.settlementFeeCbp4; + if (index == 5) return _marketState.settlementFeeCbp5; + return _marketState.settlementFeeCbp6; + } + + function _requireSettlementFeeCbpNonDecreasing( + uint256 index, + uint16 newSettlementFeeCbp, + uint16 previousSettlementFeeCbp, + uint16 nextSettlementFeeCbp + ) internal pure { + require(index == 0 || previousSettlementFeeCbp <= newSettlementFeeCbp, SettlementFeeNotMonotonic()); + require(index == 6 || newSettlementFeeCbp <= nextSettlementFeeCbp, SettlementFeeNotMonotonic()); } function setMarketContinuousFee(bytes32 id, uint256 newContinuousFee) external { From 644a82998d962f7b00b09c840149d55cca5680e8 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:15:33 +0000 Subject: [PATCH 3/4] refactor: inline fee monotonicity checks Keep the before/after breakpoint checks inline in the settlement fee setters. --- src/Midnight.sol | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index 0d393de4e..bc278d726 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -265,12 +265,17 @@ contract Midnight is IMidnight { require(_marketState.tickSpacing > 0, MarketNotCreated()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); - _requireSettlementFeeCbpNonDecreasing( - index, - newSettlementFeeCbp, - index == 0 ? 0 : _settlementFeeCbpAt(_marketState, index - 1), - index == 6 ? 0 : _settlementFeeCbpAt(_marketState, index + 1) - ); + uint16[7] memory feeCbps = [ + _marketState.settlementFeeCbp0, + _marketState.settlementFeeCbp1, + _marketState.settlementFeeCbp2, + _marketState.settlementFeeCbp3, + _marketState.settlementFeeCbp4, + _marketState.settlementFeeCbp5, + _marketState.settlementFeeCbp6 + ]; + require(index == 0 || feeCbps[index - 1] <= newSettlementFeeCbp, SettlementFeeNotMonotonic()); + require(index == 6 || newSettlementFeeCbp <= feeCbps[index + 1], SettlementFeeNotMonotonic()); if (index == 0) _marketState.settlementFeeCbp0 = newSettlementFeeCbp; else if (index == 1) _marketState.settlementFeeCbp1 = newSettlementFeeCbp; else if (index == 2) _marketState.settlementFeeCbp2 = newSettlementFeeCbp; @@ -289,33 +294,12 @@ contract Midnight is IMidnight { // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); uint16[7] storage feeCbps = defaultSettlementFeeCbp[loanToken]; - _requireSettlementFeeCbpNonDecreasing( - index, newSettlementFeeCbp, index == 0 ? 0 : feeCbps[index - 1], index == 6 ? 0 : feeCbps[index + 1] - ); + require(index == 0 || feeCbps[index - 1] <= newSettlementFeeCbp, SettlementFeeNotMonotonic()); + require(index == 6 || newSettlementFeeCbp <= feeCbps[index + 1], SettlementFeeNotMonotonic()); feeCbps[index] = newSettlementFeeCbp; emit EventsLib.SetDefaultSettlementFee(loanToken, index, newSettlementFee); } - function _settlementFeeCbpAt(MarketState storage _marketState, uint256 index) internal view returns (uint16) { - if (index == 0) return _marketState.settlementFeeCbp0; - if (index == 1) return _marketState.settlementFeeCbp1; - if (index == 2) return _marketState.settlementFeeCbp2; - if (index == 3) return _marketState.settlementFeeCbp3; - if (index == 4) return _marketState.settlementFeeCbp4; - if (index == 5) return _marketState.settlementFeeCbp5; - return _marketState.settlementFeeCbp6; - } - - function _requireSettlementFeeCbpNonDecreasing( - uint256 index, - uint16 newSettlementFeeCbp, - uint16 previousSettlementFeeCbp, - uint16 nextSettlementFeeCbp - ) internal pure { - require(index == 0 || previousSettlementFeeCbp <= newSettlementFeeCbp, SettlementFeeNotMonotonic()); - require(index == 6 || newSettlementFeeCbp <= nextSettlementFeeCbp, SettlementFeeNotMonotonic()); - } - function setMarketContinuousFee(bytes32 id, uint256 newContinuousFee) external { MarketState storage _marketState = marketState[id]; require(msg.sender == feeSetter, OnlyFeeSetter()); From 3f311c775c29338e0c59bbfb8992f11305282050 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:31:33 +0000 Subject: [PATCH 4/4] refactor: use settlement fee getter Use settlementFeeCbps(id) for the per-market neighbor checks instead of rebuilding the array inline. --- src/Midnight.sol | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index bc278d726..779f9ed80 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -265,15 +265,7 @@ contract Midnight is IMidnight { require(_marketState.tickSpacing > 0, MarketNotCreated()); // forge-lint: disable-next-item(unsafe-typecast) as newSettlementFee <= maxSettlementFee <= uint16.max * CBP uint16 newSettlementFeeCbp = uint16(newSettlementFee / CBP); - uint16[7] memory feeCbps = [ - _marketState.settlementFeeCbp0, - _marketState.settlementFeeCbp1, - _marketState.settlementFeeCbp2, - _marketState.settlementFeeCbp3, - _marketState.settlementFeeCbp4, - _marketState.settlementFeeCbp5, - _marketState.settlementFeeCbp6 - ]; + uint16[7] memory feeCbps = settlementFeeCbps(id); require(index == 0 || feeCbps[index - 1] <= newSettlementFeeCbp, SettlementFeeNotMonotonic()); require(index == 6 || newSettlementFeeCbp <= feeCbps[index + 1], SettlementFeeNotMonotonic()); if (index == 0) _marketState.settlementFeeCbp0 = newSettlementFeeCbp; @@ -921,7 +913,7 @@ contract Midnight is IMidnight { } /// @dev The settlement fee cbp values are 0 until the market is created, then set to the default value. - function settlementFeeCbps(bytes32 id) external view returns (uint16[7] memory) { + function settlementFeeCbps(bytes32 id) public view returns (uint16[7] memory) { return [ marketState[id].settlementFeeCbp0, marketState[id].settlementFeeCbp1,