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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/Midnight.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IMidnight.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ interface IMidnight {
error OfferNotStarted();
error OnlyFeeClaimer();
error OnlyFeeSetter();
error SettlementFeeNotMonotonic();
error OnlyRoleSetter();
error OnlyTickSpacingSetter();
error RatifierFail();
Expand Down
3 changes: 2 additions & 1 deletion test/MidnightBundlesTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions test/OtherFunctionsTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
108 changes: 78 additions & 30 deletions test/SettersTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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({
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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({
Expand All @@ -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");
Expand Down
23 changes: 15 additions & 8 deletions test/SettlementFeeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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");

Expand Down
7 changes: 5 additions & 2 deletions test/TakeAmountsTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
10 changes: 8 additions & 2 deletions test/TakeTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading