diff --git a/src/Midnight.sol b/src/Midnight.sol index 32c8c71f..779f9ed8 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,9 @@ 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 = settlementFeeCbps(id); + 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; @@ -280,7 +284,11 @@ 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] storage feeCbps = defaultSettlementFeeCbp[loanToken]; + 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); } @@ -905,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, diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 129db585..35a7b000 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 c480c451..95840c2d 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 8c79f4d4..09e3020c 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 d3ec3bd1..d3f31340 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 0096abee..0622afd9 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 22886ee4..23350b55 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 bb331b0d..d6f52044 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;