From 59a6c38469f2d2847e25fabebb425be271c18af3 Mon Sep 17 00:00:00 2001 From: Quentin Garchery Date: Tue, 23 Jun 2026 14:01:01 +0200 Subject: [PATCH] add verification of no subtraction underflow --- certora/confs/NoSubtractionUnderflow.conf | 23 +++ certora/specs/BalanceEffects.spec | 100 +++++----- certora/specs/BundlerRepayInvertibility.spec | 6 +- certora/specs/ContinuousFee.spec | 8 +- certora/specs/CreatedMarkets.spec | 14 +- certora/specs/Liquidate.spec | 14 +- certora/specs/LossFactor.spec | 34 +++- certora/specs/Midnight.spec | 18 +- certora/specs/NoSubtractionUnderflow.spec | 163 ++++++++++++++++ certora/specs/NotCreatedMarket.spec | 8 +- certora/specs/OnlyAuthorizedCanChange.spec | 12 +- .../OnlyAuthorizedCanChangeUpdatedValues.spec | 4 +- certora/specs/PostMaturityDebt.spec | 6 +- certora/specs/Reverts.spec | 24 +-- certora/specs/Role.spec | 21 ++ certora/specs/SplitPreservesAccounting.spec | 20 +- certora/specs/UpdateBeforeCredit.spec | 2 +- src/Midnight.sol | 182 +++++++++++------- src/interfaces/IMidnight.sol | 15 +- src/libraries/ConstantsLib.sol | 18 +- src/libraries/EventsLib.sol | 1 + src/libraries/TickLib.sol | 8 +- src/libraries/UtilsLib.sol | 9 +- src/periphery/EcrecoverAuthorizer.sol | 2 + src/periphery/MidnightBundles.sol | 2 +- .../interfaces/IEcrecoverRatifier.sol | 2 +- src/ratifiers/interfaces/ISetterRatifier.sol | 2 +- test/AuthorizationTest.sol | 6 +- test/BaseTest.sol | 31 +-- test/ContinuousFeeTest.sol | 36 ++-- ...igTest.sol => EcrecoverAuthorizerTest.sol} | 0 test/{FlashloanTest.sol => FlashLoanTest.sol} | 0 test/GateTest.sol | 18 +- test/LiquidationTest.sol | 72 ++++--- test/MaxAmountsTest.sol | 2 +- test/MidnightBundlesTest.sol | 36 ++-- test/OtherFunctionsTest.sol | 6 +- test/SettersTest.sol | 25 +++ test/TakeTest.sol | 162 ++++++++-------- test/TickGatingTest.sol | 6 +- test/TickLibTest.sol | 2 +- test/UtilsLibTest.sol | 16 +- 42 files changed, 713 insertions(+), 423 deletions(-) create mode 100644 certora/confs/NoSubtractionUnderflow.conf create mode 100644 certora/specs/NoSubtractionUnderflow.spec rename test/{SetIsAuthorizedWithSigTest.sol => EcrecoverAuthorizerTest.sol} (100%) rename test/{FlashloanTest.sol => FlashLoanTest.sol} (100%) diff --git a/certora/confs/NoSubtractionUnderflow.conf b/certora/confs/NoSubtractionUnderflow.conf new file mode 100644 index 000000000..41ef354fe --- /dev/null +++ b/certora/confs/NoSubtractionUnderflow.conf @@ -0,0 +1,23 @@ +{ + "files": [ + "certora/helpers/Utils.sol", + "src/Midnight.sol" + ], + "parametric_contracts": [ + "Midnight" + ], + "verify": "Midnight:certora/specs/NoSubtractionUnderflow.spec", + "solc": "solc-0.8.34", + "solc_via_ir": true, + "solc_evm_version": "osaka", + "optimistic_loop": true, + "loop_iter": 2, + "optimistic_hashing": true, + "hashing_length_bound": 1024, + "prover_args": [ + "-depth 5", + "-mediumTimeout 20", + "-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5}]" + ], + "msg": "No Subtraction Underflow" +} diff --git a/certora/specs/BalanceEffects.spec b/certora/specs/BalanceEffects.spec index c4f83cb13..67fe2380a 100644 --- a/certora/specs/BalanceEffects.spec +++ b/certora/specs/BalanceEffects.spec @@ -3,8 +3,8 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; function collateral(bytes32 id, address user, uint256 index) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; @@ -36,24 +36,24 @@ methods { rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 anyId, address anyUser) { bytes32 id = toId(e, market); - uint256 creditBefore = creditOf(id, user); + uint256 creditBefore = credit(id, user); uint128 updatedUserCredit; uint128 newPendingFee; uint128 userFee; updatedUserCredit, newPendingFee, userFee = updatePositionView(e, market, id, user); - uint256 anyCredit = creditOf(anyId, anyUser); - uint256 anyDebt = debtOf(anyId, anyUser); + uint256 anyCredit = credit(anyId, anyUser); + uint256 anyDebt = debt(anyId, anyUser); uint256 feeAmountBefore = continuousFeeCredit(id); updatePosition(e, market, user); - assert debtOf(anyId, anyUser) == anyDebt; - assert (anyId != id) || (anyUser != user) => creditOf(anyId, anyUser) == anyCredit; - assert creditOf(id, user) == updatedUserCredit; + assert debt(anyId, anyUser) == anyDebt; + assert (anyId != id) || (anyUser != user) => credit(anyId, anyUser) == anyCredit; + assert credit(id, user) == updatedUserCredit; assert pendingFee(id, user) == newPendingFee; assert continuousFeeCredit(id) == feeAmountBefore + userFee; - assert creditOf(id, user) <= creditBefore; + assert credit(id, user) <= creditBefore; } /// WITHDRAW /// @@ -67,15 +67,15 @@ rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBeh uint128 userFee; updatedUserCredit, _, userFee = updatePositionView(e, market, id, onBehalf); - uint256 anyCredit = creditOf(anyId, anyUser); - uint256 anyDebt = debtOf(anyId, anyUser); + uint256 anyCredit = credit(anyId, anyUser); + uint256 anyDebt = debt(anyId, anyUser); uint256 feeAmountBefore = continuousFeeCredit(id); withdraw(e, market, units, onBehalf, receiver); - assert creditOf(id, onBehalf) == updatedUserCredit - units; - assert debtOf(anyId, anyUser) == anyDebt; - assert (anyId != id) || (anyUser != onBehalf) => creditOf(anyId, anyUser) == anyCredit; + assert credit(id, onBehalf) == updatedUserCredit - units; + assert debt(anyId, anyUser) == anyDebt; + assert (anyId != id) || (anyUser != onBehalf) => credit(anyId, anyUser) == anyCredit; assert continuousFeeCredit(id) == feeAmountBefore + userFee; } @@ -90,22 +90,22 @@ rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, makerCreditBefore, _, _ = updatePositionView(e, offer.market, id, offer.maker); uint128 takerCreditBefore; takerCreditBefore, _, _ = updatePositionView(e, offer.market, id, taker); - mathint makerNetBefore = to_mathint(makerCreditBefore) - to_mathint(debtOf(id, offer.maker)); - mathint takerNetBefore = to_mathint(takerCreditBefore) - to_mathint(debtOf(id, taker)); - uint256 otherCreditBefore = creditOf(anyId, anyUser); - uint256 otherDebtBefore = debtOf(anyId, anyUser); + mathint makerNetBefore = makerCreditBefore - debt(id, offer.maker); + mathint takerNetBefore = takerCreditBefore - debt(id, taker); + uint256 otherCreditBefore = credit(anyId, anyUser); + uint256 otherDebtBefore = debt(anyId, anyUser); take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - mathint makerNetAfter = to_mathint(creditOf(id, offer.maker)) - to_mathint(debtOf(id, offer.maker)); - mathint takerNetAfter = to_mathint(creditOf(id, taker)) - to_mathint(debtOf(id, taker)); + mathint makerNetAfter = credit(id, offer.maker) - debt(id, offer.maker); + mathint takerNetAfter = credit(id, taker) - debt(id, taker); mathint makerDelta = offer.buy ? units : -units; assert makerNetAfter == makerNetBefore + makerDelta; mathint takerDelta = offer.buy ? -units : units; assert takerNetAfter == takerNetBefore + takerDelta; - assert anyId != id || (anyUser != offer.maker && anyUser != taker) => debtOf(anyId, anyUser) == otherDebtBefore; - assert anyId != id || (anyUser != offer.maker && anyUser != taker) => creditOf(anyId, anyUser) == otherCreditBefore; + assert anyId != id || (anyUser != offer.maker && anyUser != taker) => debt(anyId, anyUser) == otherDebtBefore; + assert anyId != id || (anyUser != offer.maker && anyUser != taker) => credit(anyId, anyUser) == otherCreditBefore; } /// The buyer side cannot newly become a borrower: buyer's debt is non-increasing. If buyer's credit increased, then buyer's debt is zero after the take. @@ -115,17 +115,17 @@ rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 u bytes32 id = toId(e, offer.market); address buyer = offer.buy ? offer.maker : taker; - uint256 buyerDebtBefore = debtOf(id, buyer); + uint256 buyerDebtBefore = debt(id, buyer); uint128 buyerUpdatedCreditBefore; buyerUpdatedCreditBefore, _, _ = updatePositionView(e, offer.market, id, buyer); take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - assert creditOf(id, buyer) > buyerUpdatedCreditBefore => debtOf(id, buyer) == 0; - assert creditOf(id, buyer) >= buyerUpdatedCreditBefore; - assert creditOf(id, buyer) <= buyerUpdatedCreditBefore + units; - assert debtOf(id, buyer) <= buyerDebtBefore; - assert debtOf(id, buyer) >= buyerDebtBefore - units; + assert credit(id, buyer) > buyerUpdatedCreditBefore => debt(id, buyer) == 0; + assert credit(id, buyer) >= buyerUpdatedCreditBefore; + assert credit(id, buyer) <= buyerUpdatedCreditBefore + units; + assert debt(id, buyer) <= buyerDebtBefore; + assert debt(id, buyer) >= buyerDebtBefore - units; } /// The seller side cannot newly become a lender: seller's credit is non-increasing relative to its post-update value. If seller's debt increased, then seller's credit is zero after the take. @@ -135,17 +135,17 @@ rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 bytes32 id = toId(e, offer.market); address seller = offer.buy ? taker : offer.maker; - uint256 sellerDebtBefore = debtOf(id, seller); + uint256 sellerDebtBefore = debt(id, seller); uint128 sellerUpdatedCreditBefore; sellerUpdatedCreditBefore, _, _ = updatePositionView(e, offer.market, id, seller); take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - assert debtOf(id, seller) > sellerDebtBefore => creditOf(id, seller) == 0; - assert debtOf(id, seller) >= sellerDebtBefore; - assert debtOf(id, seller) <= sellerDebtBefore + units; - assert creditOf(id, seller) <= sellerUpdatedCreditBefore; - assert creditOf(id, seller) >= sellerUpdatedCreditBefore - units; + assert debt(id, seller) > sellerDebtBefore => credit(id, seller) == 0; + assert debt(id, seller) >= sellerDebtBefore; + assert debt(id, seller) <= sellerDebtBefore + units; + assert credit(id, seller) <= sellerUpdatedCreditBefore; + assert credit(id, seller) >= sellerUpdatedCreditBefore - units; } /// REPAY /// @@ -154,15 +154,15 @@ rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data, bytes32 anyId, address anyUser) { bytes32 id = toId(e, market); - uint256 debtBefore = debtOf(id, onBehalf); - uint256 otherCreditBefore = creditOf(anyId, anyUser); - uint256 otherDebtBefore = debtOf(anyId, anyUser); + uint256 debtBefore = debt(id, onBehalf); + uint256 otherCreditBefore = credit(anyId, anyUser); + uint256 otherDebtBefore = debt(anyId, anyUser); repay(e, market, units, onBehalf, callback, data); - assert debtOf(id, onBehalf) == debtBefore - units; - assert creditOf(anyId, anyUser) == otherCreditBefore; - assert anyUser != onBehalf || anyId != id => debtOf(anyId, anyUser) == otherDebtBefore; + assert debt(id, onBehalf) == debtBefore - units; + assert credit(anyId, anyUser) == otherCreditBefore; + assert anyUser != onBehalf || anyId != id => debt(anyId, anyUser) == otherDebtBefore; } /// LIQUIDATE /// @@ -172,17 +172,17 @@ rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf rule liquidateEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bytes32 anyId, address anyUser, bool postMaturityMode) { bytes32 id = toId(e, market); - uint256 debtBefore = debtOf(id, borrower); - uint256 otherCreditBefore = creditOf(anyId, anyUser); - uint256 otherDebtBefore = debtOf(anyId, anyUser); + uint256 debtBefore = debt(id, borrower); + uint256 otherCreditBefore = credit(anyId, anyUser); + uint256 otherDebtBefore = debt(anyId, anyUser); uint256 seizedResult; uint256 repaidResult; seizedResult, repaidResult = liquidate(e, market, collateralIndex, seizedAssets, repaidUnits, borrower, postMaturityMode, receiver, callback, data); - assert debtOf(id, borrower) <= debtBefore - repaidResult; - assert creditOf(anyId, anyUser) == otherCreditBefore; - assert anyUser != borrower || anyId != id => debtOf(anyId, anyUser) == otherDebtBefore; + assert debt(id, borrower) <= debtBefore - repaidResult; + assert credit(anyId, anyUser) == otherCreditBefore; + assert anyUser != borrower || anyId != id => debt(anyId, anyUser) == otherDebtBefore; } /// ALL OTHER FUNCTIONS /// @@ -197,11 +197,11 @@ filtered { && f.selector != sig:liquidate(Midnight.Market, uint256, uint256, uint256, address, bool, address, address, bytes).selector && f.selector != sig:updatePosition(Midnight.Market, address).selector } { - uint256 creditBefore = creditOf(id, user); - uint256 debtBefore = debtOf(id, user); + uint256 creditBefore = credit(id, user); + uint256 debtBefore = debt(id, user); f(e, args); - assert creditOf(id, user) == creditBefore; - assert debtOf(id, user) == debtBefore; + assert credit(id, user) == creditBefore; + assert debt(id, user) == debtBefore; } /// SUPPLY COLLATERAL /// diff --git a/certora/specs/BundlerRepayInvertibility.spec b/certora/specs/BundlerRepayInvertibility.spec index c4212fb2e..e1ccd3ce6 100644 --- a/certora/specs/BundlerRepayInvertibility.spec +++ b/certora/specs/BundlerRepayInvertibility.spec @@ -5,7 +5,7 @@ using Midnight as midnight; methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; - function midnight.debtOf(bytes32 id, address user) external returns (uint128) envfree; + function midnight.debt(bytes32 id, address user) external returns (uint128) envfree; function midnight.isAuthorized(address authorizer, address authorized) external returns (bool) envfree; function midnight.tickSpacing(bytes32 id) external returns (uint8) envfree; @@ -45,7 +45,7 @@ rule repayAndWithdrawCollateralRepaysTargetUnits(env e, Midnight.Market market, require referralFeePct < WAD(), "PctExceeded"; bytes32 id = summaryToId(market); - uint256 debtBefore = midnight.debtOf(id, onBehalf); + uint256 debtBefore = midnight.debt(id, onBehalf); uint256 wMinusP = assert_uint256(WAD() - referralFeePct); uint256 assets = summaryMulDivDown(U, WAD(), wMinusP); @@ -59,5 +59,5 @@ rule repayAndWithdrawCollateralRepaysTargetUnits(env e, Midnight.Market market, repayAndWithdrawCollateral(e, market, assets, onBehalf, loanTokenPermit, collateralWithdrawals, collateralReceiver, referralFeePct, referralFeeRecipient); - assert midnight.debtOf(id, onBehalf) == debtBefore - U; + assert midnight.debt(id, onBehalf) == debtBefore - U; } diff --git a/certora/specs/ContinuousFee.spec b/certora/specs/ContinuousFee.spec index 5d27e6eed..847f314de 100644 --- a/certora/specs/ContinuousFee.spec +++ b/certora/specs/ContinuousFee.spec @@ -5,7 +5,7 @@ methods { function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => CVL_toId(market); - function creditOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; function continuousFee(bytes32 id) external returns (uint32) envfree; function continuousFeeCredit(bytes32 id) external returns (uint128) envfree; @@ -48,7 +48,7 @@ rule continuousFeeNotOverchargedForBuyer(env e, Midnight.Offer offer, bytes rati postUpdateCredit, postUpdatePendingFee, _ = updatePositionView(e, offer.market, id, buyer); - require pendingFee(id, buyer) <= creditOf(id, buyer), "See pendingContinuousFeeBoundedByCredit in Midnight.spec"; + require pendingFee(id, buyer) <= credit(id, buyer), "See pendingContinuousFeeBoundedByCredit in Midnight.spec"; take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); @@ -57,7 +57,7 @@ rule continuousFeeNotOverchargedForBuyer(env e, Midnight.Offer offer, bytes rati uint256 contFee = continuousFee(id); uint256 timeToMaturity = e.block.timestamp <= offer.market.maturity ? assert_uint256(offer.market.maturity - e.block.timestamp) : 0; - mathint creditDelta = creditOf(id, buyer) - postUpdateCredit; + mathint creditDelta = credit(id, buyer) - postUpdateCredit; assert pendingFee(id, buyer) == postUpdatePendingFee + (creditDelta * contFee * timeToMaturity) / WAD(); } @@ -78,7 +78,7 @@ rule pendingFeeDecreasesProportionallyForSeller(env e, Midnight.Offer offer, byt require id == lastId, "id should be derived from market"; - uint256 creditAfter = creditOf(id, seller); + uint256 creditAfter = credit(id, seller); uint256 pendingFeeAfter = pendingFee(id, seller); require creditAfter > 0 || pendingFeeAfter == 0, "See noRemainingContinuousFeeWithoutCredit in Midnight.spec"; diff --git a/certora/specs/CreatedMarkets.spec b/certora/specs/CreatedMarkets.spec index 2440c4b09..62000bf77 100644 --- a/certora/specs/CreatedMarkets.spec +++ b/certora/specs/CreatedMarkets.spec @@ -6,6 +6,7 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; function tickSpacing(bytes32) external returns (uint8) envfree; + function isLltvAllowed(uint256) external returns (bool) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function Utils.maxLif(uint256, uint256) external returns (uint256) envfree; function Utils.liquidationCursorLow() external returns (uint256) envfree; @@ -48,8 +49,6 @@ function marketIsCreated(Midnight.Market market) returns (bool) { return tickSpacing(summaryToId(market)) > 0; } -definition isLltvAllowed(uint256 lltv) returns bool = lltv == 385 * WAD() / 1000 || lltv == 625 * WAD() / 1000 || lltv == 770 * WAD() / 1000 || lltv == 860 * WAD() / 1000 || lltv == 915 * WAD() / 1000 || lltv == 945 * WAD() / 1000 || lltv == 965 * WAD() / 1000 || lltv == 980 * WAD() / 1000 || lltv == WAD(); - definition isMaxLifAllowed(uint256 lltv, uint256 maxLif) returns bool = maxLif == Utils.maxLif(lltv, Utils.liquidationCursorLow()) || maxLif == Utils.maxLif(lltv, Utils.liquidationCursorHigh()); /// RULES /// @@ -66,9 +65,18 @@ strong invariant createdMarketsHaveSortedCollaterals(Midnight.Market market, uin strong invariant createdMarketsHaveNonZeroCollaterals(Midnight.Market market, uint256 i) marketIsCreated(market) => i < market.collateralParams.length => market.collateralParams[i].token != 0; +// Show that an allowed LLTV tier is at most WAD, which holds because tiers can only be added with lltv <= WAD. +strong invariant allowedLltvIsLessThanOrEqualToOne(uint256 lltv) + isLltvAllowed(lltv) => lltv <= WAD(); + // Show that a created market has lltv <= WAD. strong invariant createdMarketsHaveLltvLessThanOrEqualToOne(Midnight.Market market, uint256 i) - marketIsCreated(market) => i < market.collateralParams.length => market.collateralParams[i].lltv <= WAD(); + marketIsCreated(market) => i < market.collateralParams.length => market.collateralParams[i].lltv <= WAD() + { + preserved { + requireInvariant allowedLltvIsLessThanOrEqualToOne(market.collateralParams[i].lltv); + } + } // Show that a created market only has allowed LLTV tiers. strong invariant createdMarketsHaveAllowedLltv(Midnight.Market market, uint256 i) diff --git a/certora/specs/Liquidate.spec b/certora/specs/Liquidate.spec index d2a01e3aa..4ca0919c2 100644 --- a/certora/specs/Liquidate.spec +++ b/certora/specs/Liquidate.spec @@ -3,8 +3,8 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function collateral(bytes32 id, address user, uint256 index) external returns (uint128) envfree; function liquidationLocked(bytes32 id, address user) external returns (bool) envfree; function isHealthy(Midnight.Market, bytes32, address) external returns (bool) envfree; @@ -49,16 +49,16 @@ rule liquidateOnlyAffectsBalancesWhenLiquidatable(env e, Midnight.Market market, address user; uint256 collateralIndex; - bool wasLiquidatable = debtOf(id, liqUser) > 0 && !liquidationLocked(id, liqUser) && (e.block.timestamp > market.maturity || !isHealthy(market, id, liqUser)); + bool wasLiquidatable = debt(id, liqUser) > 0 && !liquidationLocked(id, liqUser) && (e.block.timestamp > market.maturity || !isHealthy(market, id, liqUser)); - uint256 creditBefore = creditOf(id, user); - uint256 debtBefore = debtOf(id, user); + uint256 creditBefore = credit(id, user); + uint256 debtBefore = debt(id, user); uint256 collateralBefore = collateral(id, user, collateralIndex); liquidate(e, market, liqIndex, seizedAssets, repaidUnits, liqUser, postMaturityMode, receiver, callback, data); - uint256 creditAfter = creditOf(id, user); - uint256 debtAfter = debtOf(id, user); + uint256 creditAfter = credit(id, user); + uint256 debtAfter = debt(id, user); uint256 collateralAfter = collateral(id, user, collateralIndex); assert id == liqId => wasLiquidatable; diff --git a/certora/specs/LossFactor.spec b/certora/specs/LossFactor.spec index e1b44e8dc..4ac816dc5 100644 --- a/certora/specs/LossFactor.spec +++ b/certora/specs/LossFactor.spec @@ -5,7 +5,7 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; function totalUnits(bytes32 id) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; @@ -75,28 +75,44 @@ rule updatePositionDoesNotRevert(env e, Midnight.Market market, address user) { require marketIsCreated(market), "market must be created"; require lastLossFactor(id, user) <= currentContract.marketState[id].lossFactor, "lastLossFactor bounded by market lossFactor, already proved in Midnight.spec"; - require pendingFee(id, user) <= creditOf(id, user), "pending fee bounded by credit, already proved in Midnight.spec"; + require pendingFee(id, user) <= credit(id, user), "pending fee bounded by credit, already proved in Midnight.spec"; require currentContract.position[id][user].lastAccrual <= e.block.timestamp, "lastAccrual <= block.timestamp by timestamp monotonicity"; require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; require currentContract.marketState[id].continuousFeeCredit + pendingFee(id, user) <= max_uint128, "Total credit should be bounded by 2^128 and an increase of continuous fee credit should corresponds to a similar decrease of credit"; - require e.msg.value == 0, "setup the call"; + require e.msg.value == 0, "Midnight is not payable"; updatePosition@withrevert(e, market, user); assert !lastReverted, "updatePosition should not revert under valid state"; } +/// The loss factor computation in updatePositionView does not revert. +rule updatePositionViewDoesNotRevert(env e, Midnight.Market market, address user) { + bytes32 id = summaryToId(market); + + require lastLossFactor(id, user) <= currentContract.marketState[id].lossFactor, "lastLossFactor bounded by market lossFactor, already proved in Midnight.spec"; + require pendingFee(id, user) <= credit(id, user), "pending fee bounded by credit, already proved in Midnight.spec"; + require currentContract.position[id][user].lastAccrual <= e.block.timestamp, "lastAccrual <= block.timestamp by timestamp monotonicity"; + require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; + require currentContract.marketState[id].continuousFeeCredit + pendingFee(id, user) <= max_uint128, "Total credit should be bounded by 2^128 and an increase of continuous fee credit should corresponds to a similar decrease of credit"; + + require e.msg.value == 0, "Midnight is not payable"; + updatePositionView@withrevert(e, market, id, user); + + assert !lastReverted, "updatePositionView should not revert under valid state"; +} + /// updatePosition is idempotent: a second call in the same env leaves the relevant position state unchanged and accrues no new fee. rule updatePositionIsIdempotent(env e, Midnight.Market market, address user) { bytes32 id = summaryToId(market); - require pendingFee(id, user) <= creditOf(id, user), "see pendingContinuousFeeBoundedByCredit in Midnight.spec"; + require pendingFee(id, user) <= credit(id, user), "see pendingContinuousFeeBoundedByCredit in Midnight.spec"; require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; require currentContract.marketState[id].continuousFeeCredit + pendingFee(id, user) <= max_uint128, "see updatePositionDoesNotRevert"; // Snapshot the relevant position state after a first updatePosition. updatePosition(e, market, user); - mathint creditAfterFirst = creditOf(id, user); + mathint creditAfterFirst = credit(id, user); mathint pendingFeeAfterFirst = pendingFee(id, user); uint128 lastLossFactorAfterFirst = lastLossFactor(id, user); uint128 lastAccrualAfterFirst = currentContract.position[id][user].lastAccrual; @@ -113,7 +129,7 @@ rule updatePositionIsIdempotent(env e, Midnight.Market market, address user) { assert accruedFee == 0; // Stored position state is unchanged by the second call. - assert creditOf(id, user) == creditAfterFirst; + assert credit(id, user) == creditAfterFirst; assert pendingFee(id, user) == pendingFeeAfterFirst; assert lastLossFactor(id, user) == lastLossFactorAfterFirst; assert currentContract.position[id][user].lastAccrual == lastAccrualAfterFirst; @@ -127,10 +143,10 @@ rule updatePositionPreservesCreditWhenLossIndexCurrent(env e, Midnight.Market ma require lastLossFactor(id, user) == currentContract.marketState[id].lossFactor, "lastLossFactor synced with market"; require lastLossFactor(id, user) < max_uint128, "lossFactor not saturated"; - require pendingFee(id, user) <= creditOf(id, user), "see pendingContinuousFeeBoundedByCredit in Midnight.spec"; + require pendingFee(id, user) <= credit(id, user), "see pendingContinuousFeeBoundedByCredit in Midnight.spec"; require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; - mathint creditBefore = creditOf(id, user); + mathint creditBefore = credit(id, user); mathint pendingFeeBefore = pendingFee(id, user); uint128 newCredit; @@ -141,7 +157,7 @@ rule updatePositionPreservesCreditWhenLossIndexCurrent(env e, Midnight.Market ma // Credit and pendingFee only decrease by the accrued fee (no slashing). assert newCredit + accruedFee == creditBefore; assert newPendingFee + accruedFee == pendingFeeBefore; - assert creditOf(id, user) + accruedFee == creditBefore; + assert credit(id, user) + accruedFee == creditBefore; assert pendingFee(id, user) + accruedFee == pendingFeeBefore; } diff --git a/certora/specs/Midnight.spec b/certora/specs/Midnight.spec index 204a8e6e1..a8731d849 100644 --- a/certora/specs/Midnight.spec +++ b/certora/specs/Midnight.spec @@ -8,8 +8,8 @@ methods { function withdrawable(bytes32 id) external returns (uint128) envfree; function totalUnits(bytes32 id) external returns (uint128) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; @@ -109,13 +109,13 @@ rule lastLossFactorMonotonicallyIncreases(bytes32 id, address user, method f, en rule creditAndDebtCannotIncreaseWhenLossFactorIsMaxed(bytes32 id, address user, method f, env e, calldataarg args) { require currentContract.marketState[id].lossFactor == max_uint128, "assume loss factor is maxed out"; - uint256 creditBefore = creditOf(id, user); - uint256 debtBefore = debtOf(id, user); + uint256 creditBefore = credit(id, user); + uint256 debtBefore = debt(id, user); f(e, args); - assert creditOf(id, user) <= creditBefore; - assert debtOf(id, user) <= debtBefore; + assert credit(id, user) <= creditBefore; + assert debt(id, user) <= debtBefore; } /// INVARIANTS /// @@ -135,7 +135,7 @@ strong invariant continuousFeeBounded(bytes32 id) } strong invariant pendingContinuousFeeBoundedByCredit(bytes32 id, address user) - pendingFee(id, user) <= creditOf(id, user) + pendingFee(id, user) <= credit(id, user) { preserved with (env e) { requireInvariant continuousFeeBounded(id); @@ -150,7 +150,7 @@ strong invariant pendingContinuousFeeBoundedByCredit(bytes32 id, address user) rule noRemainingContinuousFeeWithoutCredit(bytes32 id, address user) { requireInvariant pendingContinuousFeeBoundedByCredit(id, user); - assert creditOf(id, user) == 0 => pendingFee(id, user) == 0; + assert credit(id, user) == 0 => pendingFee(id, user) == 0; } strong invariant lastLossFactorLeqMarketLossFactor(bytes32 id, address user) @@ -158,4 +158,4 @@ strong invariant lastLossFactorLeqMarketLossFactor(bytes32 id, address user) /// A user cannot have both credit and debt. strong invariant noCreditAndDebt(bytes32 id, address user) - creditOf(id, user) == 0 || debtOf(id, user) == 0; + credit(id, user) == 0 || debt(id, user) == 0; diff --git a/certora/specs/NoSubtractionUnderflow.spec b/certora/specs/NoSubtractionUnderflow.spec new file mode 100644 index 000000000..a4adf16d3 --- /dev/null +++ b/certora/specs/NoSubtractionUnderflow.spec @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +// Proves that successful calls do not underflow in any subtraction, given that every subtraction in Midnight and its +// dependencies is routed through UtilsLib.sub, and given the oracle price is bounded. + +using Utils as Utils; + +methods { + function multicall(bytes[]) external => HAVOC_ALL DELETE; + + function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function pendingFee(bytes32 id, address user) external returns (uint128) envfree; + function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; + + // Oracle integration assumption: every (collateralAmount * oraclePrice) fits in uint256. + // Storage collateral is uint128, so boundedPrice enforces the product bound against max_uint128. + function _.price() external => boundedPrice(calledContract) expect(uint256); + + // Deterministic toId: links call-site markets to validated state from touchMarket. + function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + + // Sound return bound: tickToPrice <= WAD for non-reverting calls. + function TickLib.tickToPrice(uint256) internal returns (uint256) => boundedTickPrice(); + + // Summarize mulDivDown and mulDivUp with their proven bounds (their internal subtraction is therefore not analyzed + // here; it is the `d - 1` in mulDivUp, which underflows only when d == 0, i.e. when the original code reverts too). + function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivDownSummary(x, y, d); + function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivUpSummary(x, y, d); + + // Summarize sub to track underflow. + function UtilsLib.sub(uint256 x, uint256 y) internal returns (uint256) => subSummary(x, y); +} + +/// HELPERS /// + +persistent ghost bool subUnderflow; + +definition WAD() returns uint256 = 10 ^ 18; + +definition ORACLE_PRICE_SCALE() returns uint256 = 10 ^ 36; + +// Proven in CreatedMarkets.spec (createdMarketsHaveLltvLessThanOrEqualToOne) +// and ExactMath.spec (maxLifIsAtLeastWad, maxLifIsAtMostTwoWad). +// Maturity is bounded to uint64 as a realistic timestamp assumption for underflow analysis. +function summaryToId(Midnight.Market market) returns (bytes32) { + require forall uint256 i. i < market.collateralParams.length => market.collateralParams[i].lltv <= WAD(), "proven in CreatedMarkets.spec"; + require forall uint256 i. i < market.collateralParams.length => market.collateralParams[i].maxLif >= WAD() && market.collateralParams[i].maxLif <= 2 * WAD(), "proven in ExactMath.spec"; + require market.maturity <= max_uint64, "maturity fits in uint64: realistic timestamp assumption"; + return Utils.hashMarket(market); +} + +// Bound every storage collateral (uint128) * oracle price product. +function boundedPrice(address oracle) returns uint256 { + uint256 price; + require to_mathint(price) * max_uint128 + ORACLE_PRICE_SCALE() - 1 <= max_uint256, "same as assuming that collateral * price <= uint256 with mulDivUp rounding headroom"; + return price; +} + +// Sound: tickToPrice = 1e36 / (1e18 + wExp(...)) and wExp(x) >= 0, so result <= WAD. +function boundedTickPrice() returns uint256 { + uint256 price; + require price <= WAD(), "Proven in TickToPrice.spec"; + return price; +} + +function mulDivDownSummary(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require d > 0 => to_mathint(result) * d <= to_mathint(x) * y, "proven in MulDiv.spec (mulDivDownRoundsDown)"; + require d > 0 => y <= d => result <= x, "proven in MulDiv.spec (mulDivArgumentLesserThanDenominator)"; + require d > 0 => x <= d => result <= y, "proven in MulDiv.spec (mulDivArgumentLesserThanDenominator)"; + return result; +} + +function mulDivUpSummary(uint256 x, uint256 y, uint256 d) returns uint256 { + uint256 result; + require d > 0 => to_mathint(result) * d >= to_mathint(x) * y, "proven in MulDiv.spec (mulDivUpRoundsUp)"; + require d > 0 => to_mathint(result) * d <= to_mathint(x) * y + d - 1, "proven in MulDiv.spec (mulDivUpUpperBound)"; + require d > 0 => y <= d => result <= x, "proven in MulDiv.spec (mulDivArgumentLesserThanDenominator)"; + require d > 0 => x <= d => result <= y, "proven in MulDiv.spec (mulDivArgumentLesserThanDenominator)"; + return result; +} + +// Records an underflow when x < y, and returns the exact difference otherwise. On underflow the result is left +// unconstrained so that execution can continue: this maximizes the prover's freedom to reach a reverting-free +// counterexample, exactly as the multiplication summary does for overflow. +function subSummary(uint256 x, uint256 y) returns uint256 { + uint256 result; + if (x < y) { + subUnderflow = true; + } else { + require to_mathint(result) == x - y, "exact subtraction when no underflow"; + } + return result; +} + +/// RULES /// + +// Unlike multiplication overflow (which is never intended and is ruled out purely by magnitude bounds), several of +// Midnight's subtractions underflow *by design*: the underflow is the revert that rejects an out-of-range input. These +// input-guarded entry points are excluded from the parametric rule below and listed here with where their revert +// behavior is already proven: +// - repay : `debt -= units` rejects repaying more than owed (Reverts.spec) +// - withdraw : `credit/withdrawable/totalUnits -= units` rejects over-withdrawal (Reverts.spec) +// - withdrawCollateral : `collateral -= assets` rejects withdrawing absent collateral (Reverts.spec) +// - claimSettlementFee : `claimableSettlementFee -= amount` rejects over-claiming (Role.spec) +// - claimContinuousFee : `continuousFeeCredit/totalUnits/withdrawable -= amount` rejects over-claiming (Role.spec) +// - take : `offerPrice - settlementFee` rejects buy offers priced below the fee (Reverts.spec) +// The functions that thread through _updatePosition (take/withdraw/updatePosition) or the liquidation math (liquidate) +// have their non-reverting (hence underflow-free) behavior established under explicit preconditions by +// updatePositionDoesNotRevert and liquidateLossFactorDoesNotRevert in LossFactor.spec; updatePosition(View) is proven +// underflow-free directly below under those same preconditions. + +rule noSubtractionUnderflow(method f, env e, calldataarg args) +filtered { + f -> f.selector != sig:isHealthy(Midnight.Market, bytes32, address).selector + && f.selector != sig:updatePositionView(Midnight.Market, bytes32, address).selector + && f.selector != sig:updatePosition(Midnight.Market, address).selector + && f.selector != sig:take(Midnight.Offer, bytes, uint256, address, address, address, bytes).selector + && f.selector != sig:repay(Midnight.Market, uint256, address, address, bytes).selector + && f.selector != sig:withdraw(Midnight.Market, uint256, address, address).selector + && f.selector != sig:withdrawCollateral(Midnight.Market, uint256, uint256, address, address).selector + && f.selector != sig:liquidate(Midnight.Market, uint256, uint256, uint256, address, bool, address, address, bytes).selector + && f.selector != sig:claimSettlementFee(address, uint256, address).selector + && f.selector != sig:claimContinuousFee(Midnight.Market, uint256, address).selector +} { + require !subUnderflow, "prestate: no underflow before call"; + f(e, args); + assert !subUnderflow; +} + +// isHealthy contains no subtraction, so this holds trivially; kept for parity with NoMultiplicationOverflow. +rule noSubtractionUnderflowIsHealthy(env e, Midnight.Market market, bytes32 id, address borrower) { + require !subUnderflow, "prestate: no underflow before call"; + require id == summaryToId(market), "id corresponds to market"; + isHealthy(e, market, id, borrower); + assert !subUnderflow; +} + +// Preconditions are exactly those of updatePositionDoesNotRevert in LossFactor.spec. +rule noSubtractionUnderflowUpdatePositionView(env e, Midnight.Market market, bytes32 id, address user) { + require !subUnderflow, "prestate: no underflow before call"; + require id == summaryToId(market), "id corresponds to market"; + require lastLossFactor(id, user) <= currentContract.marketState[id].lossFactor, "proven in Midnight.spec (lastLossFactorLeqMarketLossFactor)"; + require pendingFee(id, user) <= credit(id, user), "proven in Midnight.spec (pendingContinuousFeeBoundedByCredit)"; + require currentContract.position[id][user].lastAccrual <= e.block.timestamp, "lastAccrual <= block.timestamp by timestamp monotonicity"; + require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; + updatePositionView(e, market, id, user); + assert !subUnderflow; +} + +// updatePosition wraps updatePositionView (same subtractions) and additionally subtracts newCredit/newPendingFee from +// the stored credit/pendingFee, both bounded by the post-slash values. Same preconditions as above. +rule noSubtractionUnderflowUpdatePosition(env e, Midnight.Market market, address user) { + bytes32 id = summaryToId(market); + require !subUnderflow, "prestate: no underflow before call"; + require lastLossFactor(id, user) <= currentContract.marketState[id].lossFactor, "proven in Midnight.spec (lastLossFactorLeqMarketLossFactor)"; + require pendingFee(id, user) <= credit(id, user), "proven in Midnight.spec (pendingContinuousFeeBoundedByCredit)"; + require currentContract.position[id][user].lastAccrual <= e.block.timestamp, "lastAccrual <= block.timestamp by timestamp monotonicity"; + require e.block.timestamp < 2 ^ 128, "reasonable timestamp"; + updatePosition(e, market, user); + assert !subUnderflow; +} diff --git a/certora/specs/NotCreatedMarket.spec b/certora/specs/NotCreatedMarket.spec index a8cf715e1..9fbf41a50 100644 --- a/certora/specs/NotCreatedMarket.spec +++ b/certora/specs/NotCreatedMarket.spec @@ -7,8 +7,8 @@ methods { function withdrawable(bytes32) external returns (uint128) envfree; function settlementFeeCbps(bytes32) external returns (uint16[7]) envfree; function continuousFee(bytes32) external returns (uint32) envfree; - function creditOf(bytes32, address) external returns (uint128) envfree; - function debtOf(bytes32, address) external returns (uint128) envfree; + function credit(bytes32, address) external returns (uint128) envfree; + function debt(bytes32, address) external returns (uint128) envfree; function pendingFee(bytes32, address) external returns (uint128) envfree; function lastAccrual(bytes32, address) external returns (uint128) envfree; function tickSpacing(bytes32) external returns (uint8) envfree; @@ -63,10 +63,10 @@ strong invariant marketLossFactorIsEmptyIfNotCreated(bytes32 id) !marketIsCreated(id) => currentContract.marketState[id].lossFactor == 0; strong invariant marketCreditIsEmptyIfNotCreated(bytes32 id, address user) - !marketIsCreated(id) => creditOf(id, user) == 0; + !marketIsCreated(id) => credit(id, user) == 0; strong invariant marketDebtIsEmptyIfNotCreated(bytes32 id, address user) - !marketIsCreated(id) => debtOf(id, user) == 0; + !marketIsCreated(id) => debt(id, user) == 0; strong invariant marketCollateralBitmapAreEmptyIfNotCreated(bytes32 id, address user) !marketIsCreated(id) => userHasEmptyCollateralBitmap(id, user); diff --git a/certora/specs/OnlyAuthorizedCanChange.spec b/certora/specs/OnlyAuthorizedCanChange.spec index 04ebc8f10..03b6ef455 100644 --- a/certora/specs/OnlyAuthorizedCanChange.spec +++ b/certora/specs/OnlyAuthorizedCanChange.spec @@ -4,8 +4,8 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; function toId(Midnight.Market market) external returns (bytes32) envfree; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function collateral(bytes32 id, address user, uint256 index) external returns (uint128) envfree; function consumed(address user, bytes32 group) external returns (uint256) envfree; function isAuthorized(address authorizer, address authorized) external returns (bool) envfree; @@ -51,11 +51,11 @@ function CVL_isRatified(Midnight.Offer offer) returns bytes32 { rule onlyAuthorizedCanChangeCreditAndDebtExceptLiquidateAndUpdatePosition(env e, method f, calldataarg args, bytes32 id, address user) filtered { f -> f.selector != sig:liquidate(Midnight.Market, uint256, uint256, uint256, address, bool, address, address, bytes).selector && f.selector != sig:updatePosition(Midnight.Market, address).selector } { bool userIsAuthorized = user == e.msg.sender || isAuthorized(user, e.msg.sender); - uint256 creditBefore = creditOf(id, user); - uint256 debtBefore = debtOf(id, user); + uint256 creditBefore = credit(id, user); + uint256 debtBefore = debt(id, user); f(e, args); - uint256 creditAfter = creditOf(id, user); - uint256 debtAfter = debtOf(id, user); + uint256 creditAfter = credit(id, user); + uint256 debtAfter = debt(id, user); assert (creditAfter == creditBefore && debtAfter == debtBefore) || userIsAuthorized || makerRatified[user]; } diff --git a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec index dab7042d3..22dc0aa47 100644 --- a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec +++ b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec @@ -9,7 +9,7 @@ methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Position/marketState getters used to express protocol invariants as preconditions. - function creditOf(bytes32, address) external returns (uint128) envfree; + function credit(bytes32, address) external returns (uint128) envfree; function pendingFee(bytes32, address) external returns (uint128) envfree; function lastLossFactor(bytes32, address) external returns (uint128) envfree; function lastAccrual(bytes32, address) external returns (uint128) envfree; @@ -102,7 +102,7 @@ rule onlyAuthorizedCanChangeUpdatedValuesExceptLiquidate(env e, method f, callda bytes32 id = summaryToId(market); bool userIsAuthorized = user == e.msg.sender || isAuthorized(user, e.msg.sender); - require pendingFee(id, user) <= creditOf(id, user), "see pendingContinuousFeeBoundedByCredit"; + require pendingFee(id, user) <= credit(id, user), "see pendingContinuousFeeBoundedByCredit"; require lastLossFactor(id, user) <= lossFactor(id), "see lastLossFactorLeqMarketLossFactor"; require lastAccrual(id, user) <= require_uint128(e.block.timestamp), "lastAccrual <= block.timestamp by timestamp monotonicity"; diff --git a/certora/specs/PostMaturityDebt.spec b/certora/specs/PostMaturityDebt.spec index 86d77d67f..ee689b83d 100644 --- a/certora/specs/PostMaturityDebt.spec +++ b/certora/specs/PostMaturityDebt.spec @@ -5,7 +5,7 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Deterministic toId summary. @@ -31,9 +31,9 @@ function summaryToId(Midnight.Market market) returns bytes32 { rule debtCannotIncreasePostMaturity(env e, method f, calldataarg args, Midnight.Market market, address user) filtered { f -> !f.isView } { bytes32 id = summaryToId(market); - mathint debtBefore = debtOf(id, user); + mathint debtBefore = debt(id, user); f(e, args); - assert e.block.timestamp > market.maturity => debtOf(id, user) <= debtBefore; + assert e.block.timestamp > market.maturity => debt(id, user) <= debtBefore; } diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 23890e8f1..2dfcccde1 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -7,8 +7,8 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; function collateral(bytes32 id, address user, uint256) external returns (uint128) envfree; function collateralBitmap(bytes32 id, address user) external returns (uint128) envfree; function liquidationLocked(bytes32 id, address user) external returns (bool) envfree; @@ -205,7 +205,7 @@ rule oracleRevertCausesWithdrawCollateralRevert(env e, Midnight.Market market, u withdrawCollateral@withrevert(e, market, collateralIndex, assets, onBehalf, receiver); bool reverted = lastReverted; - assert debtOf(id, onBehalf) > 0 => reverted; + assert debt(id, onBehalf) > 0 => reverted; } /// If an activated collateral oracle reverts on price, isHealthy reverts when the borrower has debt. @@ -218,7 +218,7 @@ rule oracleRevertCausesIsHealthyRevert(env e, Midnight.Market market, bytes32 id isHealthy@withrevert(e, market, id, borrower); bool reverted = lastReverted; - assert debtOf(id, borrower) > 0 => reverted; + assert debt(id, borrower) > 0 => reverted; } /// If an activated collateral oracle reverts on price and take succeeds, the seller must have no debt. @@ -237,7 +237,7 @@ rule oracleRevertPreventsTakeWhenSellerHasDebt(env e, Midnight.Offer offer, byte take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - assert debtOf(id, seller) == 0; + assert debt(id, seller) == 0; } /// ORACLE RETURNS ZERO /// @@ -261,7 +261,7 @@ rule oracleZeroCausesIsHealthyReturnFalse(env e, Midnight.Market market, address bool healthy = isHealthy(e, market, id, borrower); - assert debtOf(id, borrower) > 0 => !healthy; + assert debt(id, borrower) > 0 => !healthy; } /// If all oracles return 0, withdrawCollateral reverts when the borrower has debt. @@ -273,7 +273,7 @@ rule oracleZeroPreventsWithdrawCollateralWhenBorrowerHasDebt(env e, Midnight.Mar withdrawCollateral(e, market, collateralIndex, assets, onBehalf, receiver); - assert debtOf(id, onBehalf) == 0; + assert debt(id, onBehalf) == 0; } /// If all oracles return 0 and take succeeds, the seller must have no debt. @@ -286,7 +286,7 @@ rule oracleZeroPreventsTakeWhenSellerHasDebt(env e, Midnight.Offer offer, bytes take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - assert debtOf(id, seller) == 0; + assert debt(id, seller) == 0; } /// GATE BLOCKING /// @@ -297,11 +297,11 @@ rule enterGateBlocksCreditIncrease(env e, Midnight.Offer offer, bytes ratifierDa require offer.market.enterGate != 0, "enter gate is set"; bytes32 id = summaryToId(offer.market); - uint256 creditBefore = creditOf(id, user); + uint256 creditBefore = credit(id, user); take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - uint256 creditAfter = creditOf(id, user); + uint256 creditAfter = credit(id, user); assert creditAfter <= creditBefore; } @@ -312,11 +312,11 @@ rule enterGateBlocksDebtIncrease(env e, Midnight.Offer offer, bytes ratifierData require offer.market.enterGate != 0, "enter gate is set"; bytes32 id = summaryToId(offer.market); - uint256 debtBefore = debtOf(id, user); + uint256 debtBefore = debt(id, user); take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); - uint256 debtAfter = debtOf(id, user); + uint256 debtAfter = debt(id, user); assert debtAfter <= debtBefore; } diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index 43b091754..a8206c670 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -9,6 +9,7 @@ methods { function feeSetter() external returns (address) envfree; function feeClaimer() external returns (address) envfree; function tickSpacingSetter() external returns (address) envfree; + function isLltvAllowed(uint256 lltv) external returns (bool) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; function continuousFee(bytes32 id) external returns (uint32) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; @@ -26,6 +27,8 @@ methods { /// HELPERS /// +definition WAD() returns uint256 = 10 ^ 18; + definition CBP() returns uint256 = 10 ^ 12; definition MAX_CONTINUOUS_FEE() returns uint256 = 317097919; @@ -85,6 +88,14 @@ rule roleSetterCanChangeTickSpacingSetter(env e, address newTickSpacingSetter) { assert !lastReverted => tickSpacingSetter() == newTickSpacingSetter; } +rule roleSetterCanAddLltv(env e, uint256 lltv) { + address roleSetterBefore = roleSetter(); + + addLltv@withrevert(e, lltv); + assert !lastReverted <=> e.msg.sender == roleSetterBefore && e.msg.value == 0 && lltv <= WAD(); + assert !lastReverted => isLltvAllowed(lltv); +} + /// ROLE SETTER: ACCESS CONTROL /// rule onlyRoleSetterCanChangeRoleSetter(env e, method f, calldataarg args) filtered { f -> !f.isView } { @@ -122,6 +133,16 @@ rule onlyRoleSetterCanChangeTickSpacingSetter(env e, method f, calldataarg args) assert tickSpacingSetter() != tickSpacingSetterBefore => e.msg.sender == roleSetterBefore && f.selector == sig:setTickSpacingSetter(address).selector; } +/// Allowed LLTV tiers can only be added by the role setter, and never removed. +rule onlyRoleSetterCanAddLltv(env e, method f, calldataarg args, uint256 lltv) filtered { f -> !f.isView } { + bool allowedBefore = isLltvAllowed(lltv); + address roleSetterBefore = roleSetter(); + + f(e, args); + + assert isLltvAllowed(lltv) != allowedBefore => allowedBefore == false && e.msg.sender == roleSetterBefore && f.selector == sig:addLltv(uint256).selector; +} + /// FEE SETTER: LIVENESS /// rule feeSetterCanSetMarketSettlementFee(env e, bytes32 id, uint256 index, uint256 newSettlementFee) { diff --git a/certora/specs/SplitPreservesAccounting.spec b/certora/specs/SplitPreservesAccounting.spec index 9f68588ef..ed2681548 100644 --- a/certora/specs/SplitPreservesAccounting.spec +++ b/certora/specs/SplitPreservesAccounting.spec @@ -5,8 +5,8 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function creditOf(bytes32 id, address user) external returns (uint128) envfree; - function debtOf(bytes32 id, address user) external returns (uint128) envfree; + function credit(bytes32 id, address user) external returns (uint128) envfree; + function debt(bytes32 id, address user) external returns (uint128) envfree; function totalUnits(bytes32 id) external returns (uint128) envfree; function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; function lastAccrual(bytes32 id, address user) external returns (uint128) envfree; @@ -71,10 +71,10 @@ rule splitPreservesAccounting(env e, uint256 unitsA, uint256 unitsB, uint256 uni // Path 1: take the full amount A. take(e, offer, ratifierData, unitsA, taker, receiverIfTakerIsSeller, takerCallback, takerCallbackData); - uint128 creditOfBuyer1 = creditOf(id, buyer); - uint128 debtOfBuyer1 = debtOf(id, buyer); - uint128 creditOfSeller1 = creditOf(id, seller); - uint128 debtOfSeller1 = debtOf(id, seller); + uint128 creditBuyer1 = credit(id, buyer); + uint128 debtBuyer1 = debt(id, buyer); + uint128 creditSeller1 = credit(id, seller); + uint128 debtSeller1 = debt(id, seller); uint128 totalUnits1 = totalUnits(id); uint128 buyerLossFactor1 = lastLossFactor(id, buyer); uint128 sellerLossFactor1 = lastLossFactor(id, seller); @@ -89,10 +89,10 @@ rule splitPreservesAccounting(env e, uint256 unitsA, uint256 unitsB, uint256 uni take(e, offer, ratifierData, unitsC, taker, receiverIfTakerIsSeller, takerCallback, takerCallbackData); - assert creditOfBuyer1 == creditOf(id, buyer); - assert debtOfBuyer1 == debtOf(id, buyer); - assert creditOfSeller1 == creditOf(id, seller); - assert debtOfSeller1 == debtOf(id, seller); + assert creditBuyer1 == credit(id, buyer); + assert debtBuyer1 == debt(id, buyer); + assert creditSeller1 == credit(id, seller); + assert debtSeller1 == debt(id, seller); assert totalUnits1 == totalUnits(id); assert buyerLossFactor1 == lastLossFactor(id, buyer); assert sellerLossFactor1 == lastLossFactor(id, seller); diff --git a/certora/specs/UpdateBeforeCredit.spec b/certora/specs/UpdateBeforeCredit.spec index 433365640..323eeaf67 100644 --- a/certora/specs/UpdateBeforeCredit.spec +++ b/certora/specs/UpdateBeforeCredit.spec @@ -73,7 +73,7 @@ rule creditNotStoredBeforeUpdate(env e, method f, calldataarg args, bytes32 id, /// Check that credit is never loaded before _updatePosition is called. /// The SLOADs of _updatePosition are ignored (see summary above). -rule creditNotLoadedBeforeUpdate(env e, method f, calldataarg args, bytes32 id, address user) filtered { f -> f.selector != sig:creditOf(bytes32, address).selector && f.selector != sig:updatePositionView(Midnight.Market, bytes32, address).selector && f.selector != sig:position(bytes32, address).selector } { +rule creditNotLoadedBeforeUpdate(env e, method f, calldataarg args, bytes32 id, address user) filtered { f -> f.selector != sig:credit(bytes32, address).selector && f.selector != sig:updatePositionView(Midnight.Market, bytes32, address).selector && f.selector != sig:position(bytes32, address).selector } { require !creditLoadedBeforeUpdate[id][user], "initialize the ghost variable"; f(e, args); diff --git a/src/Midnight.sol b/src/Midnight.sol index 2d3e2c4f3..f89134bee 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -51,7 +51,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// CONTINUOUS FEES /// @dev A default continuous fee (per loan token) is set on new markets. Then, the fee setter can override it. /// @dev The fee is tracked per lender via pendingFee in each position. If the market's continuous fee changes, the -/// pending fee of existing lenders is not updated (=> their fee is fixed). If the market's continuious fee is decreased +/// pending fee of existing lenders is not updated (=> their fee is fixed). If the market's continuous fee is decreased /// lenders might self-take to exit and re-enter to reduce their pending fee (at the cost of the settlement fee). /// @dev In the absence of bad debt realizations, the face value of a lender's position is credit - pendingFee. /// @dev An offer cannot be taken if its continuousFeeCap value is lower than the current market continuous fee. @@ -68,16 +68,17 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev In the "normal mode", the liquidation incentive factor (LIF) is maxLif and the liquidation amount is capped /// by what is needed to put back the position into health ("recovery close factor", or "RCF"). /// @dev The RCF condition is (omitting scaling and roundings): -/// newDebt >= newMaxDebt <=> debtOf - repaidUnits >= maxDebt - repaidUnits*LIF*LLTV -/// <=> repaidUnits <= (debtOf-maxDebt) / (1 - LIF*LLTV). +/// newDebt >= newMaxDebt <=> debt - repaidUnits >= maxDebt - repaidUnits*LIF*LLTV +/// <=> repaidUnits <= (debt-maxDebt) / (1 - LIF*LLTV). /// @dev The RCF is deactivated for small collateral amount, essentially to mitigate issues with liquidations that are /// too small compared to the gas cost. More precisely, it is deactivated if the liquidation could leave a collateral /// with a value that would not be enough to repay rcfThreshold units. Which means (omitting scaling and roundings): /// minNewCollateral * liquidatedCollatPrice / LIF < rcfThreshold /// <=> (collateral - maxRepaid * LIF / liquidatedCollatPrice) * liquidatedCollatPrice / LIF < rcfThreshold /// <=> collateral * liquidatedCollatPrice / LIF - maxRepaid < rcfThreshold -/// @dev Nothing prevents borrowers to open small positions / liquidators to leave small positions that might not be -/// profitable to liquidate because of gas cost. The RCF deactivation at rcfThreshold just prevents the systemic aspect. +/// @dev Nothing prevents borrowers from opening small positions / liquidators from leaving small positions that might +/// not be profitable to liquidate because of gas cost. The RCF deactivation at rcfThreshold just prevents the systemic +/// aspect. /// @dev In the "post-maturity mode", the LIF (liquidation incentive factor) grows linearly from 1 at maturity to maxLif /// at maturity + TIME_TO_MAX_LIF, and the RCF is deactivated. /// @dev In both modes, maxLif is used to determine if the account has some bad debt, to always assume the worst case. @@ -165,7 +166,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// revert. /// /// ROLES -/// @dev The role setter can set the role setter, fee setter, fee claimer, and tick spacing setter. +/// @dev The role setter can set the role setter, fee setter, fee claimer, and tick spacing setter, and add LLTV tiers. /// @dev The fee setter can set the default and per-market settlement fee and continuous fee. /// @dev The fee claimer can claim the settlement fee and continuous fee. /// @dev When the claimer is set, the old claimer loses the unclaimed fees. @@ -175,7 +176,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev No-ops are allowed. /// @dev Zero checks are not systematically performed. /// @dev NatSpec comments are included only when they bring clarity. -/// @dev creditOf, pendingFee, and lastLossFactor are not up to date. Use updatePositionView to get the up-to-date +/// @dev credit, pendingFee, and lastLossFactor are not up to date. Use updatePositionView to get the up-to-date /// values. /// @dev The max amount of totalUnits, collateral, credit, continuousFeeCredit and debt is type(uint128).max (~1e38). /// @dev INITIAL_CHAIN_ID is captured at construction and used in place of block.chainid when computing market ids, @@ -202,6 +203,7 @@ contract Midnight is IMidnight { mapping(address loanToken => uint16[7]) public defaultSettlementFeeCbp; mapping(address loanToken => uint32) public defaultContinuousFee; mapping(address token => uint256) public claimableSettlementFee; + mapping(uint256 lltv => bool) public isLltvAllowed; address public roleSetter; address public feeSetter; address public feeClaimer; @@ -254,6 +256,14 @@ contract Midnight is IMidnight { emit EventsLib.SetTickSpacingSetter(newTickSpacingSetter); } + /// @dev Allows a new LLTV tier. Tiers can only be added, never removed. + function addLltv(uint256 lltv) external { + require(msg.sender == roleSetter, OnlyRoleSetter()); + require(lltv <= WAD, InvalidLltv()); + isLltvAllowed[lltv] = true; + emit EventsLib.AddLltv(lltv); + } + /// @dev Refines the tick spacing of a market. Can not increase (more ticks become accessible). function setMarketTickSpacing(bytes32 id, uint256 newTickSpacing) external { require(msg.sender == tickSpacingSetter, OnlyTickSpacingSetter()); @@ -313,7 +323,7 @@ contract Midnight is IMidnight { function claimSettlementFee(address token, uint256 amount, address receiver) external { require(msg.sender == feeClaimer, OnlyFeeClaimer()); - claimableSettlementFee[token] -= amount; + claimableSettlementFee[token] = UtilsLib.sub(claimableSettlementFee[token], amount); emit EventsLib.ClaimSettlementFee(msg.sender, token, amount, receiver); SafeTransferLib.safeTransfer(token, receiver, amount); } @@ -324,9 +334,11 @@ contract Midnight is IMidnight { require(msg.sender == feeClaimer, OnlyFeeClaimer()); require(_marketState.tickSpacing > 0, MarketNotCreated()); - _marketState.continuousFeeCredit -= UtilsLib.toUint128(amount); - _marketState.totalUnits -= UtilsLib.toUint128(amount); - _marketState.withdrawable -= UtilsLib.toUint128(amount); + _marketState.continuousFeeCredit = + UtilsLib.toUint128(UtilsLib.sub(_marketState.continuousFeeCredit, UtilsLib.toUint128(amount))); + _marketState.totalUnits = UtilsLib.toUint128(UtilsLib.sub(_marketState.totalUnits, UtilsLib.toUint128(amount))); + _marketState.withdrawable = + UtilsLib.toUint128(UtilsLib.sub(_marketState.withdrawable, UtilsLib.toUint128(amount))); emit EventsLib.ClaimContinuousFee(msg.sender, id, amount, receiver); @@ -372,7 +384,7 @@ contract Midnight is IMidnight { uint256 offerPrice = TickLib.tickToPrice(offer.tick); uint256 timeToMaturity = UtilsLib.zeroFloorSub(offer.market.maturity, block.timestamp); uint256 _settlementFee = settlementFee(id, timeToMaturity); - uint256 sellerPrice = offer.buy ? offerPrice - _settlementFee : offerPrice; + uint256 sellerPrice = offer.buy ? UtilsLib.sub(offerPrice, _settlementFee) : offerPrice; uint256 buyerPrice = sellerPrice + _settlementFee; uint256 buyerAssets = offer.buy ? units.mulDivDown(buyerPrice, WAD) : units.mulDivUp(buyerPrice, WAD); uint256 sellerAssets = offer.buy ? units.mulDivDown(sellerPrice, WAD) : units.mulDivUp(sellerPrice, WAD); @@ -395,7 +407,7 @@ contract Midnight is IMidnight { uint256 buyerCreditIncrease = UtilsLib.zeroFloorSub(units, buyerPos.debt); uint256 sellerCreditDecrease = UtilsLib.min(units, sellerPos.credit); - uint256 sellerDebtIncrease = units - sellerCreditDecrease; + uint256 sellerDebtIncrease = UtilsLib.sub(units, sellerCreditDecrease); uint128 buyerPendingFeeIncrease = UtilsLib.toUint128(buyerCreditIncrease.mulDivDown(_marketState.continuousFee * timeToMaturity, WAD)); uint128 sellerPendingFeeDecrease = sellerPos.credit > 0 @@ -418,17 +430,19 @@ contract Midnight is IMidnight { SellerGatedFromIncreasingDebt() ); - buyerPos.debt -= UtilsLib.toUint128(units - buyerCreditIncrease); + buyerPos.debt = UtilsLib.toUint128( + UtilsLib.sub(buyerPos.debt, UtilsLib.toUint128(UtilsLib.sub(units, buyerCreditIncrease))) + ); buyerPos.pendingFee += buyerPendingFeeIncrease; buyerPos.credit += UtilsLib.toUint128(buyerCreditIncrease); - sellerPos.pendingFee -= sellerPendingFeeDecrease; - sellerPos.credit -= UtilsLib.toUint128(sellerCreditDecrease); + sellerPos.pendingFee = UtilsLib.toUint128(UtilsLib.sub(sellerPos.pendingFee, sellerPendingFeeDecrease)); + sellerPos.credit = UtilsLib.toUint128(UtilsLib.sub(sellerPos.credit, UtilsLib.toUint128(sellerCreditDecrease))); sellerPos.debt += UtilsLib.toUint128(sellerDebtIncrease); _marketState.totalUnits = - UtilsLib.toUint128(_marketState.totalUnits + buyerCreditIncrease - sellerCreditDecrease); - claimableSettlementFee[offer.market.loanToken] += buyerAssets - sellerAssets; + UtilsLib.toUint128(UtilsLib.sub(_marketState.totalUnits + buyerCreditIncrease, sellerCreditDecrease)); + claimableSettlementFee[offer.market.loanToken] += UtilsLib.sub(buyerAssets, sellerAssets); consumed[offer.maker][offer.group] = newConsumed; @@ -467,7 +481,9 @@ contract Midnight is IMidnight { ); } - SafeTransferLib.safeTransferFrom(offer.market.loanToken, payer, address(this), buyerAssets - sellerAssets); + SafeTransferLib.safeTransferFrom( + offer.market.loanToken, payer, address(this), UtilsLib.sub(buyerAssets, sellerAssets) + ); SafeTransferLib.safeTransferFrom(offer.market.loanToken, payer, receiver, sellerAssets); if (sellerCallback != address(0)) { @@ -503,11 +519,12 @@ contract Midnight is IMidnight { uint128 pendingFeeDecrease; if (_position.credit > 0) { pendingFeeDecrease = UtilsLib.toUint128(_position.pendingFee.mulDivUp(units, _position.credit)); - _position.pendingFee -= pendingFeeDecrease; + _position.pendingFee = UtilsLib.toUint128(UtilsLib.sub(_position.pendingFee, pendingFeeDecrease)); } - _position.credit -= UtilsLib.toUint128(units); - _marketState.withdrawable -= UtilsLib.toUint128(units); - _marketState.totalUnits -= UtilsLib.toUint128(units); + _position.credit = UtilsLib.toUint128(UtilsLib.sub(_position.credit, UtilsLib.toUint128(units))); + _marketState.withdrawable = + UtilsLib.toUint128(UtilsLib.sub(_marketState.withdrawable, UtilsLib.toUint128(units))); + _marketState.totalUnits = UtilsLib.toUint128(UtilsLib.sub(_marketState.totalUnits, UtilsLib.toUint128(units))); emit EventsLib.Withdraw(msg.sender, id, units, onBehalf, receiver, pendingFeeDecrease); @@ -520,7 +537,8 @@ contract Midnight is IMidnight { require(onBehalf == msg.sender || isAuthorized[onBehalf][msg.sender], Unauthorized()); bytes32 id = touchMarket(market); - position[id][onBehalf].debt -= UtilsLib.toUint128(units); + position[id][onBehalf].debt = + UtilsLib.toUint128(UtilsLib.sub(position[id][onBehalf].debt, UtilsLib.toUint128(units))); marketState[id].withdrawable += UtilsLib.toUint128(units); address payer = callback != address(0) ? callback : msg.sender; @@ -573,7 +591,7 @@ contract Midnight is IMidnight { address collateralToken = market.collateralParams[collateralIndex].token; Position storage _position = position[id][onBehalf]; - uint256 newCollateral = _position.collateral[collateralIndex] - assets; + uint256 newCollateral = UtilsLib.sub(_position.collateral[collateralIndex], assets); _position.collateral[collateralIndex] = UtilsLib.toUint128(newCollateral); if (newCollateral == 0 && assets > 0) { @@ -640,17 +658,25 @@ contract Midnight is IMidnight { if (badDebt > 0) { // forge-lint: disable-next-item(unsafe-typecast) as badDebt <= _position.debt - _position.debt -= uint128(badDebt); + _position.debt = UtilsLib.toUint128(UtilsLib.sub(_position.debt, uint128(badDebt))); uint256 _totalUnits = _marketState.totalUnits; uint256 _lossFactor = _marketState.lossFactor; _marketState.lossFactor = UtilsLib.toUint128( - type(uint128).max - (type(uint128).max - _lossFactor).mulDivDown(_totalUnits - badDebt, _totalUnits) + UtilsLib.sub( + type(uint128).max, + UtilsLib.sub(type(uint128).max, _lossFactor) + .mulDivDown(UtilsLib.sub(_totalUnits, badDebt), _totalUnits) + ) ); - _marketState.totalUnits -= UtilsLib.toUint128(badDebt); + _marketState.totalUnits = + UtilsLib.toUint128(UtilsLib.sub(_marketState.totalUnits, UtilsLib.toUint128(badDebt))); _marketState.continuousFeeCredit = _lossFactor < type(uint128).max ? UtilsLib.toUint128( _marketState.continuousFeeCredit - .mulDivDown(type(uint128).max - _marketState.lossFactor, type(uint128).max - _lossFactor) + .mulDivDown( + UtilsLib.sub(type(uint128).max, _marketState.lossFactor), + UtilsLib.sub(type(uint128).max, _lossFactor) + ) ) : 0; } @@ -658,7 +684,10 @@ contract Midnight is IMidnight { if (repaidUnits > 0 || seizedAssets > 0) { uint256 _maxLif = market.collateralParams[collateralIndex].maxLif; uint256 lif = postMaturityMode - ? UtilsLib.min(_maxLif, WAD + (_maxLif - WAD) * (block.timestamp - market.maturity) / TIME_TO_MAX_LIF) + ? UtilsLib.min( + _maxLif, + WAD + UtilsLib.sub(_maxLif, WAD) * UtilsLib.sub(block.timestamp, market.maturity) / TIME_TO_MAX_LIF + ) : _maxLif; if (seizedAssets > 0) { @@ -670,9 +699,9 @@ contract Midnight is IMidnight { if (!postMaturityMode) { uint256 lltv = market.collateralParams[collateralIndex].lltv; // Note that debt >= maxDebt in this branch. - // The imprecision in this computation is at most a few hundreds collateral or loan token assets. + // The imprecision in this computation is at most a few hundred collateral or loan token assets. uint256 maxRepaid = lltv < WAD - ? (_position.debt - maxDebt).mulDivUp(WAD * WAD, WAD * WAD - lif * lltv) + ? UtilsLib.sub(_position.debt, maxDebt).mulDivUp(WAD * WAD, UtilsLib.sub(WAD * WAD, lif * lltv)) : type(uint256).max; require( repaidUnits <= maxRepaid @@ -682,13 +711,15 @@ contract Midnight is IMidnight { ); } - uint128 newCollateral = _position.collateral[collateralIndex] - UtilsLib.toUint128(seizedAssets); + uint128 newCollateral = UtilsLib.toUint128( + UtilsLib.sub(_position.collateral[collateralIndex], UtilsLib.toUint128(seizedAssets)) + ); _position.collateral[collateralIndex] = newCollateral; if (newCollateral == 0 && seizedAssets > 0) { _position.collateralBitmap = _position.collateralBitmap.clearBit(collateralIndex); } _marketState.withdrawable += UtilsLib.toUint128(repaidUnits); - _position.debt -= UtilsLib.toUint128(repaidUnits); + _position.debt = UtilsLib.toUint128(UtilsLib.sub(_position.debt, UtilsLib.toUint128(repaidUnits))); } address payer = callback != address(0) ? callback : msg.sender; @@ -778,7 +809,7 @@ contract Midnight is IMidnight { address collateralToken = market.collateralParams[i].token; require(collateralToken > previousCollateralToken, CollateralParamsNotSorted()); uint256 lltv = market.collateralParams[i].lltv; - require(isLltvAllowed(lltv), LltvNotAllowed()); + require(isLltvAllowed[lltv], LltvNotAllowed()); require( market.collateralParams[i].maxLif == maxLif(lltv, LIQUIDATION_CURSOR_LOW) || market.collateralParams[i].maxLif == maxLif(lltv, LIQUIDATION_CURSOR_HIGH), @@ -815,22 +846,34 @@ contract Midnight is IMidnight { returns (uint128, uint128, uint128) { Position storage _position = position[id][user]; - uint128 credit = _position.credit; + uint128 _credit = _position.credit; uint128 _lastLossFactor = _position.lastLossFactor; uint256 postSlashCredit = _lastLossFactor < type(uint128).max - ? credit.mulDivDown(type(uint128).max - marketState[id].lossFactor, type(uint128).max - _lastLossFactor) + ? _credit.mulDivDown( + UtilsLib.sub(type(uint128).max, marketState[id].lossFactor), + UtilsLib.sub(type(uint128).max, _lastLossFactor) + ) : 0; uint128 _pendingFee = _position.pendingFee; - uint256 postSlashPendingFee = - credit > 0 ? _pendingFee - _pendingFee.mulDivUp(credit - postSlashCredit, credit) : 0; + uint256 postSlashPendingFee = _credit > 0 + ? UtilsLib.sub(_pendingFee, _pendingFee.mulDivUp(UtilsLib.sub(_credit, postSlashCredit), _credit)) + : 0; uint256 accrualEnd = UtilsLib.min(block.timestamp, market.maturity); uint128 _lastAccrual = _position.lastAccrual; // forge-lint: disable-next-item(unsafe-typecast) as fee <= pending <= credit which are uint128 position fields uint128 fee = _lastAccrual < market.maturity - ? uint128(postSlashPendingFee.mulDivDown(accrualEnd - _lastAccrual, market.maturity - _lastAccrual)) + ? uint128( + postSlashPendingFee.mulDivDown( + UtilsLib.sub(accrualEnd, _lastAccrual), UtilsLib.sub(market.maturity, _lastAccrual) + ) + ) : 0; // forge-lint: disable-next-item(unsafe-typecast) as credit and pending are <= uint128 position fields - return (uint128(postSlashCredit) - fee, uint128(postSlashPendingFee) - fee, fee); + return ( + UtilsLib.toUint128(UtilsLib.sub(uint128(postSlashCredit), fee)), + UtilsLib.toUint128(UtilsLib.sub(uint128(postSlashPendingFee), fee)), + fee + ); } /// @dev Slashes the position and accrues the continuous fee. @@ -851,8 +894,8 @@ contract Midnight is IMidnight { Position storage _position = position[id][user]; (uint128 newCredit, uint128 newPendingFee, uint128 accruedFee) = updatePositionView(market, id, user); - uint128 creditDecrease = _position.credit - newCredit; - uint128 pendingFeeDecrease = _position.pendingFee - newPendingFee; + uint128 creditDecrease = UtilsLib.toUint128(UtilsLib.sub(_position.credit, newCredit)); + uint128 pendingFeeDecrease = UtilsLib.toUint128(UtilsLib.sub(_position.pendingFee, newPendingFee)); _position.credit = newCredit; _position.lastLossFactor = marketState[id].lossFactor; @@ -871,10 +914,26 @@ contract Midnight is IMidnight { /// OTHER VIEW FUNCTIONS /// + function credit(bytes32 id, address user) external view returns (uint128) { + return position[id][user].credit; + } + + function pendingFee(bytes32 id, address user) external view returns (uint128) { + return position[id][user].pendingFee; + } + function lastLossFactor(bytes32 id, address user) external view returns (uint128) { return position[id][user].lastLossFactor; } + function lastAccrual(bytes32 id, address user) external view returns (uint128) { + return position[id][user].lastAccrual; + } + + function debt(bytes32 id, address user) external view returns (uint128) { + return position[id][user].debt; + } + function collateralBitmap(bytes32 id, address user) external view returns (uint128) { return position[id][user].collateralBitmap; } @@ -895,14 +954,6 @@ contract Midnight is IMidnight { return abi.decode(create2Address.code, (Market)); } - function creditOf(bytes32 id, address user) external view returns (uint128) { - return position[id][user].credit; - } - - function debtOf(bytes32 id, address user) external view returns (uint128) { - return position[id][user].debt; - } - function totalUnits(bytes32 id) external view returns (uint128) { return marketState[id].totalUnits; } @@ -911,14 +962,14 @@ contract Midnight is IMidnight { return marketState[id].lossFactor; } - function tickSpacing(bytes32 id) external view returns (uint8) { - return marketState[id].tickSpacing; - } - function withdrawable(bytes32 id) external view returns (uint128) { return marketState[id].withdrawable; } + function continuousFeeCredit(bytes32 id) external view returns (uint128) { + return marketState[id].continuousFeeCredit; + } + /// @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) { return [ @@ -937,16 +988,8 @@ contract Midnight is IMidnight { return marketState[id].continuousFee; } - function continuousFeeCredit(bytes32 id) external view returns (uint128) { - return marketState[id].continuousFeeCredit; - } - - function pendingFee(bytes32 id, address user) external view returns (uint128) { - return position[id][user].pendingFee; - } - - function lastAccrual(bytes32 id, address user) external view returns (uint128) { - return position[id][user].lastAccrual; + function tickSpacing(bytes32 id) external view returns (uint8) { + return marketState[id].tickSpacing; } function liquidationLocked(bytes32 id, address user) public view returns (bool) { @@ -958,9 +1001,9 @@ contract Midnight is IMidnight { /// @dev Expects the id to correspond to the market's id. function isHealthy(Market memory market, bytes32 id, address borrower) public view returns (bool) { Position storage _position = position[id][borrower]; - uint256 debt = _position.debt; + uint256 _debt = _position.debt; uint256 maxDebt; - if (debt > 0) { + if (_debt > 0) { uint128 _collateralBitmap = _position.collateralBitmap; while (_collateralBitmap != 0) { uint256 i = UtilsLib.msb(_collateralBitmap); @@ -971,7 +1014,7 @@ contract Midnight is IMidnight { _collateralBitmap = _collateralBitmap.clearBit(i); } } - return maxDebt >= debt; + return maxDebt >= _debt; } /// @dev Returns the settlement fee using piecewise linear interpolation between breakpoints. @@ -991,6 +1034,7 @@ contract Midnight is IMidnight { (180 days, 360 days, _marketState.settlementFeeCbp5 * CBP, _marketState.settlementFeeCbp6 * CBP); // forgefmt: disable-end - return (feeLower * (end - timeToMaturity) + feeUpper * (timeToMaturity - start)) / (end - start); + return (feeLower * UtilsLib.sub(end, timeToMaturity) + feeUpper * UtilsLib.sub(timeToMaturity, start)) + / UtilsLib.sub(end, start); } } diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 5f27fe0bc..7d93054da 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -77,6 +77,7 @@ interface IMidnight { error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); error InvalidFeeIndex(); + error InvalidLltv(); error InvalidMaxLif(); error InvalidOfferCaps(); error InvalidTickSpacing(); @@ -127,6 +128,7 @@ interface IMidnight { function defaultSettlementFeeCbp(address loanToken, uint256 index) external view returns (uint16); function defaultContinuousFee(address loanToken) external view returns (uint32); function claimableSettlementFee(address token) external view returns (uint256); + function isLltvAllowed(uint256 lltv) external view returns (bool); function roleSetter() external view returns (address); function feeSetter() external view returns (address); function feeClaimer() external view returns (address); @@ -140,6 +142,7 @@ interface IMidnight { function setFeeSetter(address newFeeSetter) external; function setFeeClaimer(address newFeeClaimer) external; function setTickSpacingSetter(address newTickSpacingSetter) external; + function addLltv(uint256 lltv) external; function setMarketTickSpacing(bytes32 id, uint256 newTickSpacing) external; function setMarketSettlementFee(bytes32 id, uint256 index, uint256 newSettlementFee) external; function setDefaultSettlementFee(address loanToken, uint256 index, uint256 newSettlementFee) external; @@ -149,20 +152,20 @@ interface IMidnight { function claimContinuousFee(Market memory market, uint256 amount, address receiver) external; /// ENTRY-POINTS /// - function take(Offer memory offer, bytes memory ratifierData, uint256 units, address taker, address receiverIfTakerIsSeller, address takerCallback, bytes memory takerCallbackData) external returns (uint256, uint256); + function take(Offer memory offer, bytes memory ratifierData, uint256 units, address taker, address receiverIfTakerIsSeller, address takerCallback, bytes memory takerCallbackData) external returns (uint256 buyerAssets, uint256 sellerAssets); function withdraw(Market memory market, uint256 units, address onBehalf, address receiver) external; function repay(Market memory market, uint256 units, address onBehalf, address callback, bytes memory data) external; function supplyCollateral(Market memory market, uint256 collateralIndex, uint256 assets, address onBehalf) external; function withdrawCollateral(Market memory market, uint256 collateralIndex, uint256 assets, address onBehalf, address receiver) external; - function liquidate(Market memory market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, bool postMaturityMode, address receiver, address callback, bytes memory data) external returns (uint256, uint256); + function liquidate(Market memory market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, bool postMaturityMode, address receiver, address callback, bytes memory data) external returns (uint256 outputSeizedAssets, uint256 outputRepaidUnits); function setConsumed(bytes32 group, uint256 amount, address onBehalf) external; function setIsAuthorized(address authorized, bool newIsAuthorized, address onBehalf) external; function flashLoan(address[] memory tokens, uint256[] memory assets, address callback, bytes memory data) external; function touchMarket(Market memory market) external returns (bytes32); /// SLASHING AND CONTINUOUS FEE ACCRUAL /// - function updatePositionView(Market memory market, bytes32 id, address user) external view returns (uint128, uint128, uint128); - function updatePosition(Market memory market, address user) external returns (uint128, uint128, uint128); + function updatePositionView(Market memory market, bytes32 id, address user) external view returns (uint128 newCredit, uint128 newPendingFee, uint128 accruedFee); + function updatePosition(Market memory market, address user) external returns (uint128 newCredit, uint128 newPendingFee, uint128 accruedFee); /// OTHER VIEW FUNCTIONS /// function lastLossFactor(bytes32 id, address user) external view returns (uint128); @@ -170,8 +173,8 @@ interface IMidnight { function collateral(bytes32 id, address user, uint256 index) external view returns (uint128); function toId(Market memory market) external view returns (bytes32); function toMarket(bytes32 id) external view returns (Market memory); - function creditOf(bytes32 id, address user) external view returns (uint128); - function debtOf(bytes32 id, address user) external view returns (uint128); + function credit(bytes32 id, address user) external view returns (uint128); + function debt(bytes32 id, address user) external view returns (uint128); function totalUnits(bytes32 id) external view returns (uint128); function lossFactor(bytes32 id) external view returns (uint128); function tickSpacing(bytes32 id) external view returns (uint8); diff --git a/src/libraries/ConstantsLib.sol b/src/libraries/ConstantsLib.sol index f91d005c2..6e30207ea 100644 --- a/src/libraries/ConstantsLib.sol +++ b/src/libraries/ConstantsLib.sol @@ -25,22 +25,6 @@ uint256 constant LIQUIDATION_LOCK_SLOT = uint256(keccak256("morpho.midnight.liqu bytes32 constant CALLBACK_SUCCESS = keccak256("morpho.midnight.callbackSuccess"); uint8 constant DEFAULT_TICK_SPACING = 4; -/// @dev The allowed LLTV values, copied from Morpho Blue's enabled tiers (excluding zero, including WAD). -uint256 constant LLTV_0 = 0.385e18; -uint256 constant LLTV_1 = 0.625e18; -uint256 constant LLTV_2 = 0.77e18; -uint256 constant LLTV_3 = 0.86e18; -uint256 constant LLTV_4 = 0.915e18; -uint256 constant LLTV_5 = 0.945e18; -uint256 constant LLTV_6 = 0.965e18; -uint256 constant LLTV_7 = 0.98e18; -uint256 constant LLTV_8 = 1e18; - -/// @dev Returns true if lltv is one of the allowed LLTV tiers. -function isLltvAllowed(uint256 lltv) pure returns (bool) { - return lltv == LLTV_0 || lltv == LLTV_1 || lltv == LLTV_2 || lltv == LLTV_3 || lltv == LLTV_4 || lltv == LLTV_5 || lltv == LLTV_6 || lltv == LLTV_7 || lltv == LLTV_8; -} - /// @dev Returns the max settlement fee for the given index. function maxSettlementFee(uint256 index) pure returns (uint256) { return [MAX_SETTLEMENT_FEE_0_DAYS, MAX_SETTLEMENT_FEE_1_DAY, MAX_SETTLEMENT_FEE_7_DAYS, MAX_SETTLEMENT_FEE_30_DAYS, MAX_SETTLEMENT_FEE_90_DAYS, MAX_SETTLEMENT_FEE_180_DAYS, MAX_SETTLEMENT_FEE_360_DAYS][index]; @@ -48,6 +32,6 @@ function maxSettlementFee(uint256 index) pure returns (uint256) { /// @dev Returns the max LIF for the given lltv and cursor. function maxLif(uint256 lltv, uint256 cursor) pure returns (uint256) { - return UtilsLib.mulDivDown(WAD, WAD, WAD - UtilsLib.mulDivDown(cursor, WAD - lltv, WAD)); + return UtilsLib.mulDivDown(WAD, WAD, UtilsLib.sub(WAD, UtilsLib.mulDivDown(cursor, UtilsLib.sub(WAD, lltv), WAD))); } // forgefmt: disable-end diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index ecdbc794e..6fb835273 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -11,6 +11,7 @@ library EventsLib { event SetRoleSetter(address indexed roleSetter); event SetFeeSetter(address indexed feeSetter); event SetTickSpacingSetter(address indexed tickSpacingSetter); + event AddLltv(uint256 lltv); event SetMarketTickSpacing(bytes32 indexed id_, uint256 newTickSpacing); event SetMarketSettlementFee(bytes32 indexed id_, uint256 indexed index, uint256 newSettlementFee); event SetDefaultSettlementFee(address indexed loanToken, uint256 indexed index, uint256 newSettlementFee); diff --git a/src/libraries/TickLib.sol b/src/libraries/TickLib.sol index 9f72b4089..6a09a0917 100644 --- a/src/libraries/TickLib.sol +++ b/src/libraries/TickLib.sol @@ -2,6 +2,8 @@ // Copyright (c) 2025 Morpho Association pragma solidity ^0.8.0; +import {UtilsLib} from "./UtilsLib.sol"; + int256 constant LN_ONE_PLUS_DELTA = 0.004987541511039073e18; // floor(ln(1.005) * 1e18) uint256 constant MAX_TICK = 6744; // Minimum representable price increment in WAD (1e-7 WAD). Tick prices are rounded to multiples of this value. @@ -16,7 +18,7 @@ library TickLib { /// @dev Returns x / d rounded to the nearest integer with ties rounded down, without checking for overflow. function divHalfDownUnchecked(uint256 x, uint256 d) internal pure returns (uint256) { unchecked { - return (x + (d - 1) / 2) / d; + return (x + UtilsLib.sub(d, 1) / 2) / d; } } @@ -51,7 +53,7 @@ library TickLib { } } - /// @dev Among the ticks than are multiples of spacing, returns the lowest one with a price higher or equal. + /// @dev Among the ticks that are multiples of spacing, returns the lowest one with a price higher or equal. /// @dev spacing should divide MAX_TICK. function priceToTick(uint256 price, uint256 spacing) internal pure returns (uint256) { require(price <= 1e18, PriceGreaterThanOne()); @@ -64,6 +66,6 @@ library TickLib { else high = mid; } } - return (low + spacing - 1) / spacing * spacing; + return UtilsLib.sub(low + spacing, 1) / spacing * spacing; } } diff --git a/src/libraries/UtilsLib.sol b/src/libraries/UtilsLib.sol index dcd1d32ab..df176837a 100644 --- a/src/libraries/UtilsLib.sol +++ b/src/libraries/UtilsLib.sol @@ -18,6 +18,11 @@ library UtilsLib { } } + /// @dev Returns x - y. + function sub(uint256 x, uint256 y) internal pure returns (uint256) { + return x - y; + } + /// @dev Returns (x * y) / d rounded down. function mulDivDown(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { return (x * y) / d; @@ -25,7 +30,7 @@ library UtilsLib { /// @dev Returns (x * y) / d rounded up. function mulDivUp(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { - return (x * y + (d - 1)) / d; + return (x * y + sub(d, 1)) / d; } function toUint128(uint256 x) internal pure returns (uint128) { @@ -36,7 +41,7 @@ library UtilsLib { function countBits(uint128 x) internal pure returns (uint256) { unchecked { - x = x - ((x >> 1) & 0x55555555555555555555555555555555); + x = toUint128(sub(x, (x >> 1) & 0x55555555555555555555555555555555)); x = (x & 0x33333333333333333333333333333333) + ((x >> 2) & 0x33333333333333333333333333333333); x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f; return (x * 0x01010101010101010101010101010101) >> 120; diff --git a/src/periphery/EcrecoverAuthorizer.sol b/src/periphery/EcrecoverAuthorizer.sol index 916e05a44..00823226a 100644 --- a/src/periphery/EcrecoverAuthorizer.sol +++ b/src/periphery/EcrecoverAuthorizer.sol @@ -11,6 +11,8 @@ import { EIP712_DOMAIN_TYPEHASH } from "./interfaces/IEcrecoverAuthorizer.sol"; +/// @dev Helper contract to authorize on Midnight with a signature. +/// @dev This contract must be authorized on Midnight. /// @dev If block.chainid changes (hard fork), the EIP-712 domain separator changes and previously signed authorizations /// are no longer valid. contract EcrecoverAuthorizer is IEcrecoverAuthorizer { diff --git a/src/periphery/MidnightBundles.sol b/src/periphery/MidnightBundles.sol index 7c2ca97ce..bcc5d0195 100644 --- a/src/periphery/MidnightBundles.sol +++ b/src/periphery/MidnightBundles.sol @@ -360,7 +360,7 @@ contract MidnightBundles is IMidnightBundles { } /// @dev Skips the approval entirely to save gas when the current allowance is already 2^95 - 1 (value chosen - /// because some token like COMP and UNI on ethereum have a max allowance of type(uint96).max). + /// because some tokens like COMP and UNI on Ethereum have a max allowance of type(uint96).max). /// @dev Resets to 0 before re-approving to support USDT like tokens. function forceApproveMax(address token, address spender) internal { if (IERC20(token).allowance(address(this), spender) >= type(uint96).max / 2) return; diff --git a/src/ratifiers/interfaces/IEcrecoverRatifier.sol b/src/ratifiers/interfaces/IEcrecoverRatifier.sol index f9a85ad2a..1652a16ca 100644 --- a/src/ratifiers/interfaces/IEcrecoverRatifier.sol +++ b/src/ratifiers/interfaces/IEcrecoverRatifier.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2025 Morpho Association -pragma solidity ^0.8.0; +pragma solidity >=0.5.0; import {IRatifier} from "../../interfaces/IRatifier.sol"; diff --git a/src/ratifiers/interfaces/ISetterRatifier.sol b/src/ratifiers/interfaces/ISetterRatifier.sol index ba8808713..cdb413770 100644 --- a/src/ratifiers/interfaces/ISetterRatifier.sol +++ b/src/ratifiers/interfaces/ISetterRatifier.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (c) 2025 Morpho Association -pragma solidity ^0.8.0; +pragma solidity >=0.5.0; import {IRatifier} from "../../interfaces/IRatifier.sol"; diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index 2ff493d9e..f901bdab5 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -249,7 +249,7 @@ contract AuthorizationTest is BaseTest { vm.prank(operator); midnight.take(offer, hex"", units, taker, taker, address(0), hex""); - assertEq(midnight.debtOf(id, taker), units); + assertEq(midnight.debt(id, taker), units); } function testRepayAuthorization(address authorized) public { @@ -275,7 +275,7 @@ contract AuthorizationTest is BaseTest { vm.prank(authorized); midnight.repay(market, units, borrower, address(0), hex""); - assertEq(midnight.debtOf(id, borrower), 0); + assertEq(midnight.debt(id, borrower), 0); } function testSetConsumedAuthorization(address user, address authorized) public { @@ -328,6 +328,6 @@ contract AuthorizationTest is BaseTest { // Borrower can take for themselves (no authorization needed) take(units, borrower, offer); - assertEq(midnight.debtOf(id, borrower), units); + assertEq(midnight.debt(id, borrower), units); } } diff --git a/test/BaseTest.sol b/test/BaseTest.sol index ea494bdd9..82f5de58a 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -25,15 +25,6 @@ import { ORACLE_PRICE_SCALE, MAX_COLLATERALS, LIQUIDATION_CURSOR_LOW, - LLTV_0, - LLTV_1, - LLTV_2, - LLTV_3, - LLTV_4, - LLTV_5, - LLTV_6, - LLTV_7, - LLTV_8, maxSettlementFee as _maxSettlementFee, maxLif as _maxLif } from "../src/libraries/ConstantsLib.sol"; @@ -43,6 +34,17 @@ import {EcrecoverRatifier} from "../src/ratifiers/EcrecoverRatifier.sol"; import {EcrecoverAuthorizer} from "../src/periphery/EcrecoverAuthorizer.sol"; uint256 constant MAX_TEST_AMOUNT = type(uint128).max; +/// @dev The LLTV tiers registered in tests, copied from Morpho Blue's enabled tiers (excluding zero, including WAD). +uint256 constant LLTV_0 = 0.385e18; +uint256 constant LLTV_1 = 0.625e18; +uint256 constant LLTV_2 = 0.77e18; +uint256 constant LLTV_3 = 0.86e18; +uint256 constant LLTV_4 = 0.915e18; +uint256 constant LLTV_5 = 0.945e18; +uint256 constant LLTV_6 = 0.965e18; +uint256 constant LLTV_7 = 0.98e18; +uint256 constant LLTV_8 = 1e18; + abstract contract BaseTest is Test { using UtilsLib for uint256; @@ -74,6 +76,11 @@ abstract contract BaseTest is Test { midnight.setFeeSetter(address(this)); midnight.setTickSpacingSetter(address(this)); + uint256[9] memory tiers = [LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7, LLTV_8]; + for (uint256 i = 0; i < tiers.length; i++) { + midnight.addLltv(tiers[i]); + } + uint256 _privateKey; (borrower, _privateKey) = makeAddrAndKey("borrower"); privateKey[borrower] = _privateKey; @@ -233,9 +240,9 @@ abstract contract BaseTest is Test { // then empty the market (borrow side only). vm.prank(badBorrower); midnight.setIsAuthorized(address(this), true, badBorrower); - deal(address(loanToken), address(this), midnight.debtOf(toId(market), badBorrower)); - midnight.repay(market, midnight.debtOf(toId(market), badBorrower), badBorrower, address(0), hex""); - assertEq(midnight.debtOf(toId(market), badBorrower), 0, "debt"); + deal(address(loanToken), address(this), midnight.debt(toId(market), badBorrower)); + midnight.repay(market, midnight.debt(toId(market), badBorrower), badBorrower, address(0), hex""); + assertEq(midnight.debt(toId(market), badBorrower), 0, "debt"); // reset the price. Oracle(market.collateralParams[0].oracle).setPrice(ORACLE_PRICE_SCALE); diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index da6d0724e..3507e525a 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -87,7 +87,7 @@ contract ContinuousFeeTest is BaseTest { emit EventsLib.Withdraw(lender, id, 0, lender, lender, 0); vm.prank(lender); midnight.withdraw(market, 0, lender, lender); - assertEq(midnight.creditOf(id, lender), credit - expectedFee, "credit after withdraw"); + assertEq(midnight.credit(id, lender), credit - expectedFee, "credit after withdraw"); assertEq(midnight.pendingFee(id, lender), remaining - expectedFee, "remaining after withdraw"); vm.revertToState(snap); @@ -95,7 +95,7 @@ contract ContinuousFeeTest is BaseTest { vm.expectEmit(); emit EventsLib.UpdatePosition(id, lender, expectedFee, expectedFee, expectedFee); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), credit - expectedFee, "credit after direct call"); + assertEq(midnight.credit(id, lender), credit - expectedFee, "credit after direct call"); assertEq(midnight.pendingFee(id, lender), remaining - expectedFee, "remaining after direct call"); assertEq(midnight.lastAccrual(id, lender), vm.getBlockTimestamp(), "lender lastAccrual after update"); @@ -125,7 +125,7 @@ contract ContinuousFeeTest is BaseTest { emit EventsLib.Withdraw(lender, id, 0, lender, lender, 0); vm.prank(lender); midnight.withdraw(market, 0, lender, lender); - assertEq(midnight.creditOf(id, lender), credit - remaining, "all remaining consumed (withdraw)"); + assertEq(midnight.credit(id, lender), credit - remaining, "all remaining consumed (withdraw)"); assertEq(midnight.pendingFee(id, lender), 0, "remaining is zero (withdraw)"); vm.revertToState(snap); @@ -133,7 +133,7 @@ contract ContinuousFeeTest is BaseTest { vm.expectEmit(); emit EventsLib.UpdatePosition(id, lender, remaining, remaining, remaining); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), credit - remaining, "all remaining consumed (direct)"); + assertEq(midnight.credit(id, lender), credit - remaining, "all remaining consumed (direct)"); assertEq(midnight.pendingFee(id, lender), 0, "remaining is zero (direct)"); } @@ -160,13 +160,13 @@ contract ContinuousFeeTest is BaseTest { midnight.updatePosition(market, lender); vm.warp(vm.getBlockTimestamp() + elapsed2); midnight.updatePosition(market, lender); - uint256 creditTwoAccruals = midnight.creditOf(id, lender); + uint256 creditTwoAccruals = midnight.credit(id, lender); vm.revertToState(snap); // Single accrual for same total elapsed vm.warp(vm.getBlockTimestamp() + elapsed1 + elapsed2); midnight.updatePosition(market, lender); - uint256 creditOneAccrual = midnight.creditOf(id, lender); + uint256 creditOneAccrual = midnight.credit(id, lender); assertApproxEqAbs(creditTwoAccruals, creditOneAccrual, 2, "two accruals ~ one accrual"); } @@ -181,7 +181,7 @@ contract ContinuousFeeTest is BaseTest { uint256 expectedRemaining = (uint256(feeRate) * credit).mulDivDown(ttm, WAD); assertEq(midnight.pendingFee(id, lender), expectedRemaining, "lender remaining after entry"); assertEq(midnight.pendingFee(id, borrower), 0, "borrower has no pending fee"); - assertEq(midnight.debtOf(id, borrower), credit, "debt unchanged at entry"); + assertEq(midnight.debt(id, borrower), credit, "debt unchanged at entry"); } function _makeBorrowOffer(uint256 credit2) internal view returns (Offer memory borrowOffer) { @@ -235,7 +235,7 @@ contract ContinuousFeeTest is BaseTest { midnight.updatePosition(market, lender); uint256 expectedFee = blendedRemaining.mulDivDown(elapsed, ttm); - assertApproxEqAbs(midnight.creditOf(id, lender), credit1 + credit2 - expectedFee, 1, "credit after accrual"); + assertApproxEqAbs(midnight.credit(id, lender), credit1 + credit2 - expectedFee, 1, "credit after accrual"); assertApproxEqAbs(midnight.pendingFee(id, lender), blendedRemaining - expectedFee, 1, "remaining after accrual"); } @@ -298,7 +298,7 @@ contract ContinuousFeeTest is BaseTest { take(exitAmount, lender, _makeBuyOffer(keccak256("lender-exit"))); // lender is taker = seller uint256 expectedRemaining = creditAfterAccrual > 0 ? remainingAfterAccrual - sellerPendingFeeDecrease : 0; - assertEq(midnight.creditOf(id, lender), creditAfterAccrual - exitAmount, "credit after exit"); + assertEq(midnight.credit(id, lender), creditAfterAccrual - exitAmount, "credit after exit"); assertApproxEqAbs(midnight.pendingFee(id, lender), expectedRemaining, 1, "remaining after exit"); if (exitAmount == creditAfterAccrual) { @@ -306,7 +306,7 @@ contract ContinuousFeeTest is BaseTest { } assertEq(midnight.pendingFee(id, otherLender), buyerPendingFeeIncrease, "buyer pendingFee after exit"); - assertEq(midnight.creditOf(id, otherLender), exitAmount, "buyer credit after exit"); + assertEq(midnight.credit(id, otherLender), exitAmount, "buyer credit after exit"); } function testWithdrawReducesPendingFee( @@ -350,7 +350,7 @@ contract ContinuousFeeTest is BaseTest { uint256 expectedRemaining = creditAfterAccrual > 0 ? remainingAfterAccrual - pendingFeeDecrease : 0; - assertEq(midnight.creditOf(id, lender), creditAfterAccrual - withdrawAmount, "credit after withdraw"); + assertEq(midnight.credit(id, lender), creditAfterAccrual - withdrawAmount, "credit after withdraw"); assertApproxEqAbs(midnight.pendingFee(id, lender), expectedRemaining, 1, "remaining after withdraw"); if (withdrawAmount == creditAfterAccrual) { @@ -379,13 +379,13 @@ contract ContinuousFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + elapsed1); midnight.updatePosition(market, lender); - uint256 creditBeforeSlash = midnight.creditOf(id, lender); + uint256 creditBeforeSlash = midnight.credit(id, lender); // Slash. createBadDebt(market); midnight.updatePosition(market, lender); - uint256 creditAfterSlash = midnight.creditOf(id, lender); + uint256 creditAfterSlash = midnight.credit(id, lender); vm.assume(creditAfterSlash < creditBeforeSlash); uint256 pendingAfterSlash = midnight.pendingFee(id, lender); @@ -396,7 +396,7 @@ contract ContinuousFeeTest is BaseTest { midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), creditAfterSlash - accruedFee, "credit after slash and accrual"); + assertEq(midnight.credit(id, lender), creditAfterSlash - accruedFee, "credit after slash and accrual"); assertApproxEqAbs( midnight.pendingFee(id, lender), pendingAfterSlash - accruedFee, 1, "remaining after slash and accrual" ); @@ -521,7 +521,7 @@ contract ContinuousFeeTest is BaseTest { midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), newCredit, "view matches credit"); + assertEq(midnight.credit(id, lender), newCredit, "view matches credit"); assertEq(midnight.pendingFee(id, lender), newPendingFee, "view matches pendingFee"); } @@ -553,7 +553,7 @@ contract ContinuousFeeTest is BaseTest { assertEq(returnedCredit, expectedCredit, "returned credit"); assertEq(returnedPendingFee, expectedPendingFee, "returned pendingFee"); assertEq(returnedAccruedFee, expectedAccruedFee, "returned accruedFee"); - assertEq(midnight.creditOf(id, lender), returnedCredit, "stored credit"); + assertEq(midnight.credit(id, lender), returnedCredit, "stored credit"); assertEq(midnight.pendingFee(id, lender), returnedPendingFee, "stored pendingFee"); assertEq(midnight.continuousFeeCredit(id), expectedContinuousFeeCredit, "continuousFeeCredit"); } @@ -614,7 +614,7 @@ contract ContinuousFeeTest is BaseTest { offer.continuousFeeCap = continuousFeeCap; take(units, borrower, offer); - assertEq(midnight.debtOf(id, borrower), units, "borrower took on debt"); + assertEq(midnight.debt(id, borrower), units, "borrower took on debt"); } function testTakeRevertsWhenSellOfferContinuousFeeCapExceeded(uint256 feeRate, uint256 continuousFeeCap) public { @@ -646,6 +646,6 @@ contract ContinuousFeeTest is BaseTest { offer.continuousFeeCap = continuousFeeCap; take(units, lender, offer); // lender is the buyer, otherBorrower (maker) is the seller. - assertEq(midnight.creditOf(id, lender), units, "lender gained credit"); + assertEq(midnight.credit(id, lender), units, "lender gained credit"); } } diff --git a/test/SetIsAuthorizedWithSigTest.sol b/test/EcrecoverAuthorizerTest.sol similarity index 100% rename from test/SetIsAuthorizedWithSigTest.sol rename to test/EcrecoverAuthorizerTest.sol diff --git a/test/FlashloanTest.sol b/test/FlashLoanTest.sol similarity index 100% rename from test/FlashloanTest.sol rename to test/FlashLoanTest.sol diff --git a/test/GateTest.sol b/test/GateTest.sol index 8c6597ed6..f587e8125 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -123,8 +123,8 @@ contract GateTest is BaseTest { take(units, lender, borrowerOffer); - assertGt(midnight.creditOf(gatedId, lender), 0, "lender should have credit"); - assertGt(midnight.debtOf(gatedId, borrower), 0, "borrower should have debt"); + assertGt(midnight.credit(gatedId, lender), 0, "lender should have credit"); + assertGt(midnight.debt(gatedId, borrower), 0, "borrower should have debt"); } function testEnterGateAllowsTakeWhenLenderHadCreditBefore(uint256 units) public { @@ -134,7 +134,7 @@ contract GateTest is BaseTest { collateralize(gatedMarket, borrower, units); take(units, lender, borrowerOffer); - assertGt(midnight.creditOf(gatedId, lender), 0, "lender should already have credit"); + assertGt(midnight.credit(gatedId, lender), 0, "lender should already have credit"); gate.setWhitelisted(lender, false); gate.setWhitelisted(borrower, false); @@ -149,7 +149,7 @@ contract GateTest is BaseTest { collateralize(gatedMarket, borrower, units); take(units, lender, borrowerOffer); - assertGt(midnight.debtOf(gatedId, borrower), 0, "borrower should already have debt"); + assertGt(midnight.debt(gatedId, borrower), 0, "borrower should already have debt"); gate.setWhitelisted(lender, false); gate.setWhitelisted(borrower, false); @@ -184,7 +184,7 @@ contract GateTest is BaseTest { take(units, borrower, otherBorrowerOffer); - assertEq(midnight.debtOf(gatedId, borrower), 0, "borrower should have exited debt"); + assertEq(midnight.debt(gatedId, borrower), 0, "borrower should have exited debt"); } function testNoGateCheckWhenBothExit(uint256 units) public { @@ -224,7 +224,7 @@ contract GateTest is BaseTest { deal(address(loanToken), otherBorrower, units); take(units, otherBorrower, exitOffer); - assertEq(midnight.debtOf(gatedId, otherBorrower), 0, "otherBorrower should have exited"); + assertEq(midnight.debt(gatedId, otherBorrower), 0, "otherBorrower should have exited"); } function testNoGateCheckOnRepay(uint256 units) public { @@ -241,7 +241,7 @@ contract GateTest is BaseTest { vm.prank(borrower); midnight.repay(gatedMarket, units, borrower, address(0), hex""); - assertEq(midnight.debtOf(gatedId, borrower), 0, "borrower should have repaid"); + assertEq(midnight.debt(gatedId, borrower), 0, "borrower should have repaid"); } function testNoGateCheckOnWithdraw(uint256 units) public { @@ -261,7 +261,7 @@ contract GateTest is BaseTest { vm.prank(lender); midnight.withdraw(gatedMarket, units, lender, lender); - assertEq(midnight.creditOf(gatedId, lender), 0, "lender should have withdrawn"); + assertEq(midnight.credit(gatedId, lender), 0, "lender should have withdrawn"); } // --- Liquidator gate tests --- @@ -317,7 +317,7 @@ contract GateTest is BaseTest { take(units, borrower, ungatedLenderOffer); bytes32 ungatedId = toId(market); - assertGt(midnight.debtOf(ungatedId, borrower), 0); + assertGt(midnight.debt(ungatedId, borrower), 0); } // --- Market identity tests --- diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index cb95826ff..09e57f6c4 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -7,7 +7,6 @@ import { ORACLE_PRICE_SCALE, TIME_TO_MAX_LIF, MAX_CONTINUOUS_FEE, - LLTV_8, LIQUIDATION_CURSOR_LOW, CALLBACK_SUCCESS } from "../src/libraries/ConstantsLib.sol"; @@ -16,7 +15,7 @@ import {IdLib} from "../src/libraries/IdLib.sol"; import {IOracle} from "../src/interfaces/IOracle.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {Oracle} from "./helpers/Oracle.sol"; -import {BaseTest, MAX_TEST_AMOUNT} from "./BaseTest.sol"; +import {BaseTest, MAX_TEST_AMOUNT, LLTV_8} from "./BaseTest.sol"; import {stdError} from "../lib/forge-std/src/StdError.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; @@ -99,7 +98,7 @@ contract LiquidationTest is BaseTest { uint256 collatBefore = midnight.collateral(id, borrower, 0); midnight.liquidate(market, 1, 0, 0, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), 0); + assertEq(midnight.debt(id, borrower), 0); assertEq(midnight.collateral(id, borrower, 0), collatBefore); assertEq(midnight.collateral(id, borrower, 1), 0); } @@ -211,7 +210,7 @@ contract LiquidationTest is BaseTest { "seized assets" ); - assertEq(midnight.debtOf(id, borrower), units - repaidUnits); + assertEq(midnight.debt(id, borrower), units - repaidUnits); assertEq(midnight.collateral(id, borrower, 0), initialCollateral - seizedAssets); } @@ -244,7 +243,7 @@ contract LiquidationTest is BaseTest { ); assertEq(seizedAssets, seized, "seized assets"); - assertEq(midnight.debtOf(id, borrower), units - repaidUnits, "debt"); + assertEq(midnight.debt(id, borrower), units - repaidUnits, "debt"); assertEq(midnight.collateral(id, borrower, 0), initialCollateral - seizedAssets, "collateral"); } @@ -354,11 +353,11 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, 0, 0, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), units - expectedBadDebt, "debt"); + assertEq(midnight.debt(id, borrower), units - expectedBadDebt, "debt"); assertEq(midnight.totalUnits(id), units - expectedBadDebt, "total units"); - assertEq(midnight.creditOf(id, lender), units, "lender units"); + assertEq(midnight.credit(id, lender), units, "lender units"); midnight.updatePosition(market, lender); - assertApproxEqAbs(midnight.creditOf(id, lender), units - expectedBadDebt, 1, "lender units after slashing"); + assertApproxEqAbs(midnight.credit(id, lender), units - expectedBadDebt, 1, "lender units after slashing"); } function testLiquidateEmitsLossFactorAndContinuousFeeCredit(uint256 units) public { @@ -417,7 +416,7 @@ contract LiquidationTest is BaseTest { emit EventsLib.UpdatePosition(id, lender, units - expectedCredit, 0, 0); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), expectedCredit, "credit"); + assertEq(midnight.credit(id, lender), expectedCredit, "credit"); assertEq(midnight.lastLossFactor(id, lender), lossFactor, "last loss factor"); } @@ -432,11 +431,11 @@ contract LiquidationTest is BaseTest { (, uint256 repaid) = midnight.liquidate(market, 0, seized, 0, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), debtAfterBadDebt - repaid, "debt"); + assertEq(midnight.debt(id, borrower), debtAfterBadDebt - repaid, "debt"); assertEq(midnight.totalUnits(id), debtAfterBadDebt, "total units"); - assertEq(midnight.creditOf(id, lender), units, "lender units"); + assertEq(midnight.credit(id, lender), units, "lender units"); midnight.updatePosition(market, lender); - assertApproxEqAbs(midnight.creditOf(id, lender), debtAfterBadDebt, 1, "lender units after slashing"); + assertApproxEqAbs(midnight.credit(id, lender), debtAfterBadDebt, 1, "lender units after slashing"); } function testLiquidateWithBadDebtRepaidInput(uint256 units, uint256 repaid, uint256 liquidationOraclePrice) public { @@ -454,11 +453,11 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, 0, repaid, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), debtAfterBadDebt - repaid, "debt"); + assertEq(midnight.debt(id, borrower), debtAfterBadDebt - repaid, "debt"); assertEq(midnight.totalUnits(id), debtAfterBadDebt, "total units"); - assertEq(midnight.creditOf(id, lender), units, "lender units"); + assertEq(midnight.credit(id, lender), units, "lender units"); midnight.updatePosition(market, lender); - assertApproxEqAbs(midnight.creditOf(id, lender), debtAfterBadDebt, 1, "lender units after slashing"); + assertApproxEqAbs(midnight.credit(id, lender), debtAfterBadDebt, 1, "lender units after slashing"); } // Check that if there is bad debt it is possible to seize almost all collateral. @@ -473,7 +472,7 @@ contract LiquidationTest is BaseTest { market, 0, midnight.collateral(id, borrower, 0), 0, borrower, false, address(this), address(0), "" ); - assertApproxEqAbs(midnight.debtOf(id, borrower), 0, 1e3, "almost all remaining debt repaid"); + assertApproxEqAbs(midnight.debt(id, borrower), 0, 1e3, "almost all remaining debt repaid"); assertApproxEqAbs( midnight.collateral(id, borrower, 0).mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE), 0, @@ -504,7 +503,7 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, 0, repaid, borrower, true, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), units - repaid, "debt"); + assertEq(midnight.debt(id, borrower), units - repaid, "debt"); assertEq( midnight.collateral(id, borrower, 0), initialCollateral @@ -535,7 +534,7 @@ contract LiquidationTest is BaseTest { uint256 lif = WAD + (market.collateralParams[0].maxLif - WAD) * delay / TIME_TO_MAX_LIF; - assertEq(midnight.debtOf(id, borrower), units - repaid, "debt"); + assertEq(midnight.debt(id, borrower), units - repaid, "debt"); assertEq( midnight.collateral(id, borrower, 0), initialCollateral - repaid.mulDivDown(lif, WAD).mulDivDown(ORACLE_PRICE_SCALE, liquidationOraclePrice), @@ -572,7 +571,7 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, 0, min(maxR, units), borrower, false, address(this), address(0), ""); uint256 remainingCollateral = midnight.collateral(id, borrower, 0); - uint256 remainingDebt = midnight.debtOf(id, borrower); + uint256 remainingDebt = midnight.debt(id, borrower); uint256 newMaxDebt = remainingCollateral.mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE) .mulDivDown(market.collateralParams[0].lltv, WAD); // After max repayment the position should be just healthy or almost healthy (within rounding tolerance). @@ -619,7 +618,7 @@ contract LiquidationTest is BaseTest { // Full liquidation should succeed because remaining debt < rcfThreshold. midnight.liquidate(market, 0, 0, units, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(toId(market), borrower), 0, "debt should be zero"); + assertEq(midnight.debt(toId(market), borrower), 0, "debt should be zero"); } /// @dev When rcfThreshold <= remaining debt after max repayment, recovery close factor is enforced. @@ -705,7 +704,7 @@ contract LiquidationTest is BaseTest { bytes32 borrowerSlot = keccak256(abi.encode(borrower, intermediateSlot)); vm.store(address(midnight), bytes32(uint256(borrowerSlot) + 2), bytes32(units)); - assertEq(midnight.debtOf(id, borrower), units, "debt"); + assertEq(midnight.debt(id, borrower), units, "debt"); // Collateralize with both collateralParams. @@ -730,7 +729,7 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, collateral1, 0, borrower, false, address(this), address(0), ""); } - uint256 debtAfter = midnight.debtOf(id, borrower); + uint256 debtAfter = midnight.debt(id, borrower); uint256 collateralAfter = midnight.collateral(id, borrower, 0); assertTrue(debtAfter == 0 || collateralAfter == 0, "either debt repaid or collateral seized"); } @@ -836,11 +835,11 @@ contract LiquidationTest is BaseTest { collateralize(market, borrower, units); setupMarket(market, units); - uint256 creditBefore = midnight.creditOf(id, lender); + uint256 creditBefore = midnight.credit(id, lender); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), creditBefore, "credit unchanged"); + assertEq(midnight.credit(id, lender), creditBefore, "credit unchanged"); } function testSlashNoCredit(uint256 units) public { @@ -851,15 +850,14 @@ contract LiquidationTest is BaseTest { Oracle(market.collateralParams[0].oracle).setPrice(badDebtPriceDown(units)); midnight.liquidate(market, 0, 0, 0, borrower, false, address(this), address(0), ""); - assertEq(midnight.creditOf(id, borrower), 0, "no credit before"); - uint256 debtBefore = midnight.debtOf(id, borrower); + assertEq(midnight.credit(id, borrower), 0, "no credit before"); + uint256 debtBefore = midnight.debt(id, borrower); uint128 oblLossFactor = midnight.lossFactor(id); assertGt(oblLossFactor, midnight.lastLossFactor(id, borrower), "last loss factor stale before"); - midnight.updatePosition(market, borrower); - assertEq(midnight.creditOf(id, borrower), 0, "no credit after"); - assertEq(midnight.debtOf(id, borrower), debtBefore, "debt unchanged"); + assertEq(midnight.credit(id, borrower), 0, "no credit after"); + assertEq(midnight.debt(id, borrower), debtBefore, "debt unchanged"); assertEq(midnight.lastLossFactor(id, borrower), oblLossFactor, "last loss factor synced"); } @@ -871,15 +869,15 @@ contract LiquidationTest is BaseTest { Oracle(market.collateralParams[0].oracle).setPrice(badDebtPriceDown(units)); midnight.liquidate(market, 0, 0, 0, borrower, false, address(this), address(0), ""); - uint256 creditBeforeSlash = midnight.creditOf(id, lender); + uint256 creditBeforeSlash = midnight.credit(id, lender); midnight.updatePosition(market, lender); - uint256 creditAfterFirstSlash = midnight.creditOf(id, lender); + uint256 creditAfterFirstSlash = midnight.credit(id, lender); uint128 lastLossFactorAfterFirstSlash = midnight.lastLossFactor(id, lender); assertLt(creditAfterFirstSlash, creditBeforeSlash, "first slash reduced credit"); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), creditAfterFirstSlash, "credit unchanged"); + assertEq(midnight.credit(id, lender), creditAfterFirstSlash, "credit unchanged"); assertEq(midnight.lastLossFactor(id, lender), lastLossFactorAfterFirstSlash, "last loss factor unchanged"); } @@ -893,12 +891,12 @@ contract LiquidationTest is BaseTest { Oracle(market.collateralParams[0].oracle).setPrice(0); midnight.liquidate(market, 0, 0, 0, borrower, false, address(this), address(0), ""); - assertEq(midnight.debtOf(id, borrower), 0, "debt"); + assertEq(midnight.debt(id, borrower), 0, "debt"); assertEq(midnight.totalUnits(id), 0, "total units"); uint128 _lossFactor = midnight.lossFactor(id); assertEq(_lossFactor, type(uint128).max, "loss factor"); midnight.updatePosition(market, lender); - assertEq(midnight.creditOf(id, lender), 0, "credit after slashing"); + assertEq(midnight.credit(id, lender), 0, "credit after slashing"); // withdrawCollateral still works uint256 collateral = midnight.collateral(id, borrower, 0); @@ -913,7 +911,7 @@ contract LiquidationTest is BaseTest { /// @dev Bad debt as computed in liquidate function _badDebt() internal view returns (uint256) { - uint256 badDebt = midnight.debtOf(id, borrower); + uint256 badDebt = midnight.debt(id, borrower); uint128 collateralBitmap = midnight.collateralBitmap(id, borrower); while (collateralBitmap != 0) { uint256 i = UtilsLib.msb(collateralBitmap); @@ -988,10 +986,10 @@ contract LiquidationTest is BaseTest { // Drop price so position is unhealthy. Oracle(market.collateralParams[0].oracle).setPrice(ORACLE_PRICE_SCALE / 2); - uint256 debtBefore = midnight.debtOf(id, borrower); + uint256 debtBefore = midnight.debt(id, borrower); // Non-zero seizedAssets exercises the recovery close factor path. midnight.liquidate(market, 0, 1, 0, borrower, false, address(this), address(0), ""); - assertLt(midnight.debtOf(id, borrower), debtBefore, "debt should decrease after liquidation"); + assertLt(midnight.debt(id, borrower), debtBefore, "debt should decrease after liquidation"); } function testLiquidateNoDebtReverts() public { diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index 50b12afc5..fe1cefae8 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -74,7 +74,7 @@ contract MaxAmountsTest is BaseTest { take(amount, lender, borrowerOffer); assertEq(midnight.totalUnits(id), amount, "total units at max"); - assertEq(midnight.debtOf(id, borrower), amount, "debt at max"); + assertEq(midnight.debt(id, borrower), amount, "debt at max"); } function testTakeAboveMaxAmountReverts() public { diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index c480c4515..47f2a8443 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -175,8 +175,8 @@ contract MidnightBundlesTest is BaseTest { uint256 consumed0 = midnight.consumed(offers[0].maker, offers[0].group); uint256 consumed1 = midnight.consumed(offers[1].maker, offers[1].group); assertEq(consumed0, fromOffer0, "consumed offer 0"); - assertEq(consumed0 + consumed1, midnight.debtOf(id, borrower), "total consumed"); - assertEq(midnight.debtOf(id, borrower), units, "debt"); + assertEq(consumed0 + consumed1, midnight.debt(id, borrower), "total consumed"); + assertEq(midnight.debt(id, borrower), units, "debt"); } else { vm.prank(borrower); vm.expectRevert(IMidnightBundles.OutOfOffers.selector); @@ -231,7 +231,7 @@ contract MidnightBundlesTest is BaseTest { uint256 consumed0 = midnight.consumed(offers[0].maker, offers[0].group); uint256 consumed1 = midnight.consumed(offers[1].maker, offers[1].group); assertEq(consumed0, fromOffer0, "consumed offer 0"); - assertEq(consumed0 + consumed1, midnight.debtOf(id, borrower), "total consumed"); + assertEq(consumed0 + consumed1, midnight.debt(id, borrower), "total consumed"); assertEq(loanToken.balanceOf(lender), type(uint256).max - targetBuyerAssets, "lender balance"); } else { vm.prank(lender); @@ -284,7 +284,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(loanToken.allowance(lender, address(midnightBundles)), 0); assertEq(loanToken.allowance(lender, PERMIT2), 0); assertEq(loanToken.balanceOf(lender), type(uint256).max - targetBuyerAssets); - assertEq(midnight.creditOf(id, lender), units); + assertEq(midnight.credit(id, lender), units); } function testBuyBuyerAssetsTargetPermit() public { @@ -316,7 +316,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(loanToken.allowance(lender, address(midnightBundles)), 0); assertEq(loanToken.balanceOf(lender), type(uint256).max - targetBuyerAssets); - assertEq(midnight.creditOf(id, lender), units); + assertEq(midnight.credit(id, lender), units); } function testBuyUnitsTargetPermit2() public { @@ -350,7 +350,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(loanToken.allowance(lender, address(midnightBundles)), 0); assertEq(loanToken.allowance(lender, PERMIT2), 0); assertEq(loanToken.balanceOf(lender), type(uint256).max - maxBuyerAssets); - assertEq(midnight.creditOf(id, lender), units); + assertEq(midnight.credit(id, lender), units); } function testBuyUnitsTargetInconsistentMarket() public { @@ -468,7 +468,7 @@ contract MidnightBundlesTest is BaseTest { uint256 consumed0 = midnight.consumed(offers[0].maker, offers[0].group); uint256 consumed1 = midnight.consumed(offers[1].maker, offers[1].group); assertEq(consumed0, fromOffer0, "consumed offer 0"); - assertEq(consumed0 + consumed1, midnight.debtOf(id, borrower), "total consumed"); + assertEq(consumed0 + consumed1, midnight.debt(id, borrower), "total consumed"); assertEq(loanToken.balanceOf(borrower), targetSellerAssets, "borrower balance"); } else { vm.prank(borrower); @@ -543,7 +543,7 @@ contract MidnightBundlesTest is BaseTest { referrer ); - assertEq(midnight.debtOf(id, borrower), units, "units filled"); + assertEq(midnight.debt(id, borrower), units, "units filled"); assertEq(loanToken.balanceOf(borrower), expectedFilledBuyerAssets, "maker receipt"); assertEq(loanToken.balanceOf(referrer), expectedFee, "referrer fee"); assertEq( @@ -577,7 +577,7 @@ contract MidnightBundlesTest is BaseTest { units, 0, borrower, receiver, new CollateralSupply[](0), takes, referralFeePct, referrer ); - assertEq(midnight.debtOf(id, borrower), units, "units sold"); + assertEq(midnight.debt(id, borrower), units, "units sold"); assertEq(loanToken.balanceOf(receiver), expectedFilledSellerAssets - expectedFee, "receiver net"); assertEq(loanToken.balanceOf(referrer), expectedFee, "referrer fee"); assertEq(loanToken.balanceOf(address(midnightBundles)), 0, "bundler residual"); @@ -705,7 +705,7 @@ contract MidnightBundlesTest is BaseTest { market, assets, borrower, _noPermit(), new CollateralWithdrawal[](0), address(0), referralFeePct, referrer ); - assertEq(midnight.debtOf(id, borrower), units - expectedUnits, "debt"); + assertEq(midnight.debt(id, borrower), units - expectedUnits, "debt"); assertEq(loanToken.balanceOf(referrer), expectedFee, "referrer fee"); assertEq(loanToken.balanceOf(borrower), 0, "borrower spent assets"); assertEq(loanToken.balanceOf(address(midnightBundles)), 0, "bundler residual"); @@ -742,7 +742,7 @@ contract MidnightBundlesTest is BaseTest { market, assets, borrower, _noPermit(), new CollateralWithdrawal[](0), address(0), referralFeePct, referrer ); - assertEq(midnight.debtOf(id, borrower), 0, "debt fully repaid"); + assertEq(midnight.debt(id, borrower), 0, "debt fully repaid"); assertEq(loanToken.balanceOf(referrer), expectedFee, "referrer fee"); assertEq(loanToken.balanceOf(borrower), 0, "borrower spent assets"); assertEq(loanToken.balanceOf(address(midnightBundles)), 0, "bundler residual"); @@ -910,7 +910,7 @@ contract MidnightBundlesTest is BaseTest { for (uint256 i; i < numCollaterals; i++) { assertEq(midnight.collateral(id, borrower, i), supplies[i].assets); } - assertEq(midnight.debtOf(id, borrower), units); + assertEq(midnight.debt(id, borrower), units); } function testSellUnitsTargetPermit2() public { @@ -943,7 +943,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(ERC20(collateralToken).allowance(borrower, address(midnightBundles)), 0); assertEq(ERC20(collateralToken).allowance(borrower, PERMIT2), 0); assertEq(midnight.collateral(id, borrower, 0), amount); - assertEq(midnight.debtOf(id, borrower), units); + assertEq(midnight.debt(id, borrower), units); } function testRepay(uint256 units, uint256 repayUnits, uint256 withdrawAssets) public { @@ -985,7 +985,7 @@ contract MidnightBundlesTest is BaseTest { market, repayUnits, borrower, _noPermit(), withdrawals, collateralReceiver, 0, address(0) ); - assertEq(midnight.debtOf(id, borrower), units - repayUnits, "debt"); + assertEq(midnight.debt(id, borrower), units - repayUnits, "debt"); assertEq(midnight.collateral(id, borrower, 0), collateralAmount - withdrawAssets, "remaining collateral"); assertEq( ERC20(market.collateralParams[0].token).balanceOf(collateralReceiver), withdrawAssets, "collateral receiver" @@ -1204,7 +1204,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(midnight.consumed(offers[0].maker, offers[0].group), 100, "consumed offer 0"); assertEq(midnight.consumed(offers[1].maker, offers[1].group), 30, "consumed offer 1"); - assertEq(midnight.debtOf(id, borrower), 100, "debt"); + assertEq(midnight.debt(id, borrower), 100, "debt"); } function testSellSellerAssetsTargetPartiallyConsumed() public { @@ -1238,7 +1238,7 @@ contract MidnightBundlesTest is BaseTest { // Offer 0 should hit its cap (consumed 30 + filled up to 70). assertEq(consumed0, 100, "consumed offer 0"); // Total newly filled units equal the borrower's debt. - assertEq(consumed0 - 30 + consumed1, midnight.debtOf(id, borrower), "total consumed"); + assertEq(consumed0 - 30 + consumed1, midnight.debt(id, borrower), "total consumed"); assertEq(loanToken.balanceOf(borrower), targetSellerAssets, "borrower balance"); } @@ -1277,7 +1277,7 @@ contract MidnightBundlesTest is BaseTest { assertEq(midnight.consumed(offers[0].maker, offers[0].group), 100, "consumed offer 0"); assertEq(midnight.consumed(offers[1].maker, offers[1].group), 30, "consumed offer 1"); - assertEq(midnight.debtOf(id, borrower), 100, "debt"); + assertEq(midnight.debt(id, borrower), 100, "debt"); } function testBuyBuyerAssetsTargetPartiallyConsumed() public { @@ -1316,6 +1316,6 @@ contract MidnightBundlesTest is BaseTest { uint256 consumed0 = midnight.consumed(offers[0].maker, offers[0].group); uint256 consumed1 = midnight.consumed(offers[1].maker, offers[1].group); assertEq(consumed0, 100, "consumed offer 0"); - assertEq(consumed0 - 30 + consumed1, midnight.debtOf(id, borrower), "total consumed"); + assertEq(consumed0 - 30 + consumed1, midnight.debt(id, borrower), "total consumed"); } } diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 03660e36a..08827607d 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -135,7 +135,7 @@ contract OtherFunctionsTest is BaseTest { vm.prank(borrower); midnight.repay(market, repaid, borrower, address(0), hex""); - assertEq(midnight.debtOf(id, borrower), units - repaid); + assertEq(midnight.debt(id, borrower), units - repaid); assertEq(midnight.withdrawable(id), repaid); assertEq(loanToken.balanceOf(address(midnight)), repaid); assertEq(loanToken.balanceOf(borrower), 0); @@ -158,7 +158,7 @@ contract OtherFunctionsTest is BaseTest { vm.prank(caller); midnight.repay(market, repaid, borrower, address(callback), data); - assertEq(midnight.debtOf(id, borrower), units - repaid); + assertEq(midnight.debt(id, borrower), units - repaid); assertEq(callback.recordedId(), id, "id"); assertEq(toId(callback.recordedMarket()), id, "market"); assertEq(callback.recordedOnBehalf(), borrower, "onBehalf"); @@ -174,7 +174,7 @@ contract OtherFunctionsTest is BaseTest { vm.prank(lender); midnight.withdraw(market, withdraw, lender, lender); - assertEq(midnight.creditOf(id, lender), units - withdraw, "creditOf"); + assertEq(midnight.credit(id, lender), units - withdraw, "credit"); assertEq(midnight.withdrawable(id), 0, "withdrawable"); assertEq(midnight.totalUnits(id), units - withdraw, "totalUnits"); assertEq(loanToken.balanceOf(address(midnight)), 0, "balance of midnight"); diff --git a/test/SettersTest.sol b/test/SettersTest.sol index f6e59c6d2..bcd8ac21a 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { + WAD, MAX_CONTINUOUS_FEE, MAX_SETTLEMENT_FEE_0_DAYS, MAX_SETTLEMENT_FEE_1_DAY, @@ -77,6 +78,30 @@ contract SettersTest is BaseTest { midnight.setFeeSetter(makeAddr("newFeeSetter")); } + function testAddLltvSuccess(uint256 lltv) public { + lltv = bound(lltv, 0, WAD); + vm.assume(!midnight.isLltvAllowed(lltv)); + + vm.expectEmit(); + emit EventsLib.AddLltv(lltv); + + midnight.addLltv(lltv); + assertTrue(midnight.isLltvAllowed(lltv)); + } + + function testAddLltvOnlyRoleSetter(address rdm, uint256 lltv) public { + vm.assume(rdm != address(this)); + vm.prank(rdm); + vm.expectRevert(IMidnight.OnlyRoleSetter.selector); + midnight.addLltv(lltv); + } + + function testAddLltvInvalidLltv(uint256 lltv) public { + lltv = bound(lltv, WAD + 1, type(uint256).max); + vm.expectRevert(IMidnight.InvalidLltv.selector); + midnight.addLltv(lltv); + } + function testSetSettlementFeeSuccess( address loanToken, uint256 postMaturityFee, diff --git a/test/TakeTest.sol b/test/TakeTest.sol index e8c820b0b..3d9abbc40 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -224,8 +224,8 @@ contract TakeTest is BaseTest { take(units, lender, borrowerOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, borrower), units, "borrower debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, borrower), units, "borrower debt"); assertEq(midnight.totalUnits(id), units, "total units"); assertEq(loanToken.balanceOf(borrower), expectedAssets, "borrower balance"); assertEq(loanToken.balanceOf(lender), 0, "lender balance"); @@ -244,8 +244,8 @@ contract TakeTest is BaseTest { take(units, borrower, lenderOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, borrower), units, "borrower debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, borrower), units, "borrower debt"); assertEq(midnight.totalUnits(id), units, "total units"); assertEq(loanToken.balanceOf(borrower), expectedAssets, "borrower balance"); assertEq(loanToken.balanceOf(lender), 0, "lender balance"); @@ -262,7 +262,7 @@ contract TakeTest is BaseTest { uint256 buyerAssets = units.mulDivDown(price, WAD); otherLenderUnits = bound(otherLenderUnits, units, max(units, maxAssets)); setupOtherUsers(market, otherLenderUnits); - uint256 actualOtherLenderCredit = midnight.creditOf(id, otherLender); + uint256 actualOtherLenderCredit = midnight.credit(id, otherLender); deal(address(loanToken), lender, buyerAssets + 1); otherLenderOffer.buy = false; otherLenderOffer.maxUnits = type(uint256).max; @@ -271,10 +271,10 @@ contract TakeTest is BaseTest { take(units, lender, otherLenderOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, lender), 0, "lender debt"); - assertEq(midnight.creditOf(id, otherLender), actualOtherLenderCredit - units, "other lender units"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, lender), 0, "lender debt"); + assertEq(midnight.credit(id, otherLender), actualOtherLenderCredit - units, "other lender units"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -286,7 +286,7 @@ contract TakeTest is BaseTest { uint256 buyerAssets = units.mulDivDown(price, WAD); otherLenderUnits = bound(otherLenderUnits, units, max(units, maxAssets)); setupOtherUsers(market, otherLenderUnits); - uint256 actualOtherLenderCredit = midnight.creditOf(id, otherLender); + uint256 actualOtherLenderCredit = midnight.credit(id, otherLender); deal(address(loanToken), lender, buyerAssets + 1); lenderOffer.maxUnits = type(uint256).max; lenderOffer.tick = tick; @@ -294,10 +294,10 @@ contract TakeTest is BaseTest { take(units, otherLender, lenderOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, lender), 0, "lender debt"); - assertEq(midnight.creditOf(id, otherLender), actualOtherLenderCredit - units, "other lender units"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, lender), 0, "lender debt"); + assertEq(midnight.credit(id, otherLender), actualOtherLenderCredit - units, "other lender units"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -306,7 +306,7 @@ contract TakeTest is BaseTest { otherLenderUnits = bound(otherLenderUnits, 1, maxAssets - 1); units = bound(units, otherLenderUnits + 1, maxAssets); setupOtherUsers(market, otherLenderUnits); - uint256 otherLenderCredit = midnight.creditOf(id, otherLender); + uint256 otherLenderCredit = midnight.credit(id, otherLender); uint256 price = TickLib.tickToPrice(MAX_TICK); deal(address(loanToken), lender, units.mulDivUp(price, WAD)); collateralize(market, otherLender, units); @@ -316,9 +316,9 @@ contract TakeTest is BaseTest { take(units, lender, otherLenderOffer); // otherLender crossed from lender to borrower. - assertEq(midnight.creditOf(id, otherLender), 0, "otherLender credit"); - assertEq(midnight.debtOf(id, otherLender), units - otherLenderCredit, "otherLender debt"); - assertEq(midnight.creditOf(id, lender), units, "lender credit"); + assertEq(midnight.credit(id, otherLender), 0, "otherLender credit"); + assertEq(midnight.debt(id, otherLender), units - otherLenderCredit, "otherLender debt"); + assertEq(midnight.credit(id, lender), units, "lender credit"); assertEq(midnight.totalUnits(id), totalUnitsBefore + units - otherLenderCredit, "total units"); } @@ -329,7 +329,7 @@ contract TakeTest is BaseTest { tick = bound(tick, 0, MAX_TICK); existingUnits = bound(existingUnits, units, max(units, maxAssets)); setupOtherUsers(market, existingUnits); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); collateralize(market, borrower, units); borrowerOffer.maxUnits = type(uint256).max; borrowerOffer.tick = tick; @@ -339,8 +339,8 @@ contract TakeTest is BaseTest { take(units, otherBorrower, borrowerOffer); - assertEq(midnight.debtOf(id, borrower), units, "borrower debt"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.debt(id, borrower), units, "borrower debt"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -349,7 +349,7 @@ contract TakeTest is BaseTest { tick = bound(tick, 0, MAX_TICK); existingUnits = bound(existingUnits, units, max(units, maxAssets)); setupOtherUsers(market, existingUnits); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); collateralize(market, borrower, units); otherBorrowerOffer.maxUnits = type(uint256).max; otherBorrowerOffer.tick = tick; @@ -357,8 +357,8 @@ contract TakeTest is BaseTest { take(units, borrower, otherBorrowerOffer); - assertEq(midnight.debtOf(id, borrower), units, "borrower debt"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.debt(id, borrower), units, "borrower debt"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -367,7 +367,7 @@ contract TakeTest is BaseTest { otherUnits = bound(otherUnits, 1, maxAssets - 1); units = bound(units, otherUnits + 1, maxAssets); setupOtherUsers(market, otherUnits); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); uint256 price = TickLib.tickToPrice(MAX_TICK); deal(address(loanToken), otherBorrower, units.mulDivUp(price, WAD)); collateralize(market, borrower, units); @@ -377,9 +377,9 @@ contract TakeTest is BaseTest { take(units, otherBorrower, borrowerOffer); // otherBorrower crossed from borrower to lender. - assertEq(midnight.debtOf(id, otherBorrower), 0, "otherBorrower debt"); - assertEq(midnight.creditOf(id, otherBorrower), units - otherBorrowerDebt, "otherBorrower credit"); - assertEq(midnight.debtOf(id, borrower), units, "borrower debt"); + assertEq(midnight.debt(id, otherBorrower), 0, "otherBorrower debt"); + assertEq(midnight.credit(id, otherBorrower), units - otherBorrowerDebt, "otherBorrower credit"); + assertEq(midnight.debt(id, borrower), units, "borrower debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore + units - otherBorrowerDebt, "total units"); } @@ -393,8 +393,8 @@ contract TakeTest is BaseTest { uint256 buyerAssets = units.mulDivUp(price, WAD); existingUnits = bound(existingUnits, units, max(units, maxAssets)); setupOtherUsers(market, existingUnits); - uint256 otherLenderCredit = midnight.creditOf(id, otherLender); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherLenderCredit = midnight.credit(id, otherLender); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); otherLenderOffer.maxUnits = type(uint256).max; otherLenderOffer.tick = tick; @@ -402,8 +402,8 @@ contract TakeTest is BaseTest { take(units, otherBorrower, otherLenderOffer); - assertEq(midnight.creditOf(id, otherLender), otherLenderCredit - units, "otherLender units"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.credit(id, otherLender), otherLenderCredit - units, "otherLender units"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), otherBorrowerDebt - units, "total units"); assertEq(loanToken.balanceOf(otherLender), buyerAssets, "otherLender balance"); } @@ -416,16 +416,16 @@ contract TakeTest is BaseTest { uint256 buyerAssets = units.mulDivDown(price, WAD); existingUnits = bound(existingUnits, units, max(units, maxAssets)); setupOtherUsers(market, existingUnits); - uint256 otherLenderCredit = midnight.creditOf(id, otherLender); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherLenderCredit = midnight.credit(id, otherLender); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); otherBorrowerOffer.maxUnits = type(uint256).max; otherBorrowerOffer.tick = tick; take(units, otherLender, otherBorrowerOffer); - assertEq(midnight.creditOf(id, otherLender), otherLenderCredit - units, "otherLender units"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.credit(id, otherLender), otherLenderCredit - units, "otherLender units"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), otherBorrowerDebt - units, "total units"); assertEq(loanToken.balanceOf(otherLender), buyerAssets, "otherLender balance"); } @@ -459,8 +459,8 @@ contract TakeTest is BaseTest { function testBuy2PostMaturity() public { uint256 units = 100; setupOtherUsers(market, units); - assertEq(midnight.creditOf(id, otherLender), units, "other lender credit"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, otherLender), units, "other lender credit"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertTrue(midnight.isHealthy(market, id, otherLender), "other lender healthy"); uint256 totalUnitsBefore = midnight.totalUnits(id); @@ -472,18 +472,18 @@ contract TakeTest is BaseTest { take(units, lender, otherLenderOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, lender), 0, "lender debt"); - assertEq(midnight.creditOf(id, otherLender), 0, "other lender units"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, lender), 0, "lender debt"); + assertEq(midnight.credit(id, otherLender), 0, "other lender units"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } function testSell2PostMaturity() public { uint256 units = 100; setupOtherUsers(market, units); - assertEq(midnight.creditOf(id, otherLender), units, "other lender credit"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, otherLender), units, "other lender credit"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertTrue(midnight.isHealthy(market, id, otherLender), "other lender healthy"); uint256 totalUnitsBefore = midnight.totalUnits(id); @@ -495,10 +495,10 @@ contract TakeTest is BaseTest { take(units, otherLender, lenderOffer); - assertEq(midnight.creditOf(id, lender), units, "lender units"); - assertEq(midnight.debtOf(id, lender), 0, "lender debt"); - assertEq(midnight.creditOf(id, otherLender), 0, "other lender units"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, lender), units, "lender units"); + assertEq(midnight.debt(id, lender), 0, "lender debt"); + assertEq(midnight.credit(id, otherLender), 0, "other lender units"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -535,10 +535,10 @@ contract TakeTest is BaseTest { function testBuy4PostMaturity() public { uint256 units = 100; setupOtherUsers(market, units); - assertEq(midnight.creditOf(id, otherLender), units, "other lender credit"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, otherLender), units, "other lender credit"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertTrue(midnight.isHealthy(market, id, otherLender), "other lender healthy"); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); uint256 timestamp = market.maturity + 1; vm.warp(timestamp); @@ -548,18 +548,18 @@ contract TakeTest is BaseTest { take(units, otherBorrower, otherLenderOffer); - assertEq(midnight.creditOf(id, otherLender), 0, "otherLender units"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.credit(id, otherLender), 0, "otherLender units"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), otherBorrowerDebt - units, "total units"); } function testSell4PostMaturity() public { uint256 units = 100; setupOtherUsers(market, units); - assertEq(midnight.creditOf(id, otherLender), units, "other lender credit"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, otherLender), units, "other lender credit"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertTrue(midnight.isHealthy(market, id, otherLender), "other lender healthy"); - uint256 otherBorrowerDebt = midnight.debtOf(id, otherBorrower); + uint256 otherBorrowerDebt = midnight.debt(id, otherBorrower); uint256 timestamp = market.maturity + 1; vm.warp(timestamp); @@ -569,8 +569,8 @@ contract TakeTest is BaseTest { take(units, otherLender, otherBorrowerOffer); - assertEq(midnight.creditOf(id, otherLender), 0, "otherLender units"); - assertEq(midnight.debtOf(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); + assertEq(midnight.credit(id, otherLender), 0, "otherLender units"); + assertEq(midnight.debt(id, otherBorrower), otherBorrowerDebt - units, "otherBorrower debt"); assertEq(midnight.totalUnits(id), otherBorrowerDebt - units, "total units"); } @@ -588,14 +588,14 @@ contract TakeTest is BaseTest { deal(address(loanToken), otherBorrower, exitUnits.mulDivUp(price, WAD)); collateralize(market, borrower, exitUnits); - uint256 debtBefore = midnight.debtOf(id, otherBorrower); + uint256 debtBefore = midnight.debt(id, otherBorrower); uint256 totalUnitsBefore = midnight.totalUnits(id); take(exitUnits, borrower, otherBorrowerOffer); - assertEq(midnight.debtOf(id, borrower), exitUnits, "borrower debt"); - assertEq(midnight.creditOf(id, otherBorrower), 0, "otherBorrower units"); - assertEq(midnight.debtOf(id, otherBorrower), debtBefore - exitUnits, "otherBorrower debt"); + assertEq(midnight.debt(id, borrower), exitUnits, "borrower debt"); + assertEq(midnight.credit(id, otherBorrower), 0, "otherBorrower units"); + assertEq(midnight.debt(id, otherBorrower), debtBefore - exitUnits, "otherBorrower debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -622,15 +622,15 @@ contract TakeTest is BaseTest { uint256 price = TickLib.tickToPrice(MAX_TICK); deal(address(loanToken), lender, exitUnits.mulDivUp(price, WAD)); - uint256 creditBefore = midnight.creditOf(id, otherLender); + uint256 creditBefore = midnight.credit(id, otherLender); uint256 totalUnitsBefore = midnight.totalUnits(id); take(exitUnits, lender, otherLenderOffer); - assertEq(midnight.creditOf(id, lender), exitUnits, "lender units"); - assertEq(midnight.debtOf(id, lender), 0, "lender debt"); - assertEq(midnight.creditOf(id, otherLender), creditBefore - exitUnits, "other lender units"); - assertEq(midnight.debtOf(id, otherLender), 0, "other lender debt"); + assertEq(midnight.credit(id, lender), exitUnits, "lender units"); + assertEq(midnight.debt(id, lender), 0, "lender debt"); + assertEq(midnight.credit(id, otherLender), creditBefore - exitUnits, "other lender units"); + assertEq(midnight.debt(id, otherLender), 0, "other lender debt"); assertEq(midnight.totalUnits(id), totalUnitsBefore, "total units"); } @@ -752,8 +752,8 @@ contract TakeTest is BaseTest { take(units, address(this), borrowerOffer); take(units, address(this), lenderOffer); - assertEq(midnight.creditOf(id, address(this)), 0, "credit"); - assertEq(midnight.debtOf(id, address(this)), 0, "debt"); + assertEq(midnight.credit(id, address(this)), 0, "credit"); + assertEq(midnight.debt(id, address(this)), 0, "debt"); } // address(this) makes an arbitrage for 2 crossed offers. @@ -779,8 +779,8 @@ contract TakeTest is BaseTest { take(units, address(this), lenderOffer); take(units, address(this), borrowerOffer); - assertEq(midnight.creditOf(id, address(this)), 0, "credit"); - assertEq(midnight.debtOf(id, address(this)), 0, "debt"); + assertEq(midnight.credit(id, address(this)), 0, "credit"); + assertEq(midnight.debt(id, address(this)), 0, "debt"); } function testBuyPastMaturity(uint256 timestamp) public { @@ -1036,8 +1036,8 @@ contract TakeTest is BaseTest { vm.prank(lender); midnight.setConsumed(lenderOffer.group, lenderOffer.maxAssets, lender); - uint256 lenderCreditBefore = midnight.creditOf(id, lender); - uint256 borrowerDebtBefore = midnight.debtOf(id, borrower); + uint256 lenderCreditBefore = midnight.credit(id, lender); + uint256 borrowerDebtBefore = midnight.debt(id, borrower); uint256 totalUnitsBefore = midnight.totalUnits(id); uint256 lenderBalBefore = loanToken.balanceOf(lender); uint256 borrowerBalBefore = loanToken.balanceOf(borrower); @@ -1052,8 +1052,8 @@ contract TakeTest is BaseTest { assertEq(loanToken.balanceOf(lender), lenderBalBefore); assertEq(loanToken.balanceOf(borrower), borrowerBalBefore); // But position state strictly changed: - assertGt(midnight.creditOf(id, lender), lenderCreditBefore); - assertGt(midnight.debtOf(id, borrower), borrowerDebtBefore); + assertGt(midnight.credit(id, lender), lenderCreditBefore); + assertGt(midnight.debt(id, borrower), borrowerDebtBefore); assertGt(midnight.totalUnits(id), totalUnitsBefore); } @@ -1286,7 +1286,7 @@ contract TakeTest is BaseTest { assertFalse(callback.liquidateSucceeded()); assertEq(callback.liquidateErrorSelector(), IMidnight.NotLiquidatable.selector); - assertEq(midnight.debtOf(id, borrower), units); + assertEq(midnight.debt(id, borrower), units); assertEq(midnight.collateral(id, borrower, 0), collateral); } @@ -1319,7 +1319,7 @@ contract TakeTest is BaseTest { assertFalse(callback.liquidateSucceeded()); assertEq(callback.liquidateErrorSelector(), IMidnight.NotLiquidatable.selector); assertTrue(midnight.liquidationLocked(id, borrower) == false); - assertEq(midnight.debtOf(id, borrower), 2 * units); + assertEq(midnight.debt(id, borrower), 2 * units); assertEq(midnight.collateral(id, borrower, 0), 2 * collateral); } @@ -1411,8 +1411,8 @@ contract TakeTest is BaseTest { (uint256 buyerAssets, uint256 sellerAssets) = take(units, lender, borrowerOffer); assertEq(buyerAssets, 0, "buyerAssets"); assertEq(sellerAssets, 0, "sellerAssets"); - assertEq(midnight.creditOf(id, lender), units, "creditOf"); - assertEq(midnight.debtOf(id, borrower), units, "debtOf"); + assertEq(midnight.credit(id, lender), units, "credit"); + assertEq(midnight.debt(id, borrower), units, "debt"); } // fee>0, buy, units @@ -1441,8 +1441,8 @@ contract TakeTest is BaseTest { (uint256 buyerAssets, uint256 sellerAssets) = take(units, lender, borrowerOffer); assertEq(buyerAssets, expectedBuyerAssets, "buyerAssets"); assertEq(sellerAssets, 0, "sellerAssets"); - assertEq(midnight.creditOf(id, lender), units, "creditOf"); - assertEq(midnight.debtOf(id, borrower), units, "debtOf"); + assertEq(midnight.credit(id, lender), units, "credit"); + assertEq(midnight.debt(id, borrower), units, "debt"); } function testTakeWithAddressZero(uint256 units) public { diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index b013c724f..015d1405a 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -71,7 +71,7 @@ contract TickGatingTest is BaseTest { deal(address(loanToken), lender, units.mulDivUp(price, WAD)); collateralize(market, borrower, units); take(units, borrower, offer); - assertEq(midnight.creditOf(id, lender), units); + assertEq(midnight.credit(id, lender), units); } function testTakeRevertsAtInaccessibleTick() public { @@ -122,7 +122,7 @@ contract TickGatingTest is BaseTest { // Now should succeed. take(units, borrower, offer); - assertEq(midnight.creditOf(id, lender), units); + assertEq(midnight.credit(id, lender), units); } // --- setMarketTickSpacing governance --- @@ -183,6 +183,6 @@ contract TickGatingTest is BaseTest { collateralize(market, borrower, units); take(units, borrower, offer2); - assertEq(midnight.creditOf(id, lender), 2 * units); + assertEq(midnight.credit(id, lender), 2 * units); } } diff --git a/test/TickLibTest.sol b/test/TickLibTest.sol index 1f839741c..8a80d29f3 100644 --- a/test/TickLibTest.sol +++ b/test/TickLibTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import {BaseTest} from "./BaseTest.sol"; -import {console} from "forge-std/Test.sol"; +import {console} from "../lib/forge-std/src/Test.sol"; import {TickLib} from "../src/libraries/TickLib.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {MAX_TICK, PRICE_ROUNDING_STEP} from "../src/libraries/TickLib.sol"; diff --git a/test/UtilsLibTest.sol b/test/UtilsLibTest.sol index 39de0c3bd..d6fed580e 100644 --- a/test/UtilsLibTest.sol +++ b/test/UtilsLibTest.sol @@ -4,20 +4,8 @@ pragma solidity ^0.8.0; import {Test, stdError} from "../lib/forge-std/src/Test.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {LN_ONE_PLUS_DELTA, MAX_TICK, TickLib} from "../src/libraries/TickLib.sol"; -import { - WAD, - LIQUIDATION_CURSOR_LOW, - LIQUIDATION_CURSOR_HIGH, - LLTV_0, - LLTV_1, - LLTV_2, - LLTV_3, - LLTV_4, - LLTV_5, - LLTV_6, - LLTV_7, - maxLif -} from "../src/libraries/ConstantsLib.sol"; +import {WAD, LIQUIDATION_CURSOR_LOW, LIQUIDATION_CURSOR_HIGH, maxLif} from "../src/libraries/ConstantsLib.sol"; +import {LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7} from "./BaseTest.sol"; contract UtilsLibTest is Test { int256 internal constant WEXP_LN2 = 0.693147180559945309e18;