From 91a7392992b063766ddee65bbf1f5911b20b2384 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:10:53 +0000 Subject: [PATCH 01/29] feat: embed market id domain fields Requested by: <@U02NB58F6R3> --- certora/specs/BalanceEffects.spec | 2 +- certora/specs/BundlerRepayInvertibility.spec | 2 +- certora/specs/CollateralBitmap.spec | 2 +- certora/specs/Consume.spec | 2 +- certora/specs/ContinuousFee.spec | 4 +- certora/specs/CreatedMarkets.spec | 4 +- certora/specs/EmptyOffer.spec | 2 +- certora/specs/Healthiness.spec | 8 ++-- certora/specs/Liquidate.spec | 4 +- certora/specs/LiquidationBoundedByLIF.spec | 2 +- certora/specs/LiquidationProfitability.spec | 2 +- certora/specs/LossFactor.spec | 4 +- certora/specs/Midnight.spec | 4 +- certora/specs/NoDivisionByZero.spec | 6 +-- certora/specs/NoMultiplicationOverflow.spec | 2 +- certora/specs/OnlyAuthorizedCanChange.spec | 3 +- .../OnlyAuthorizedCanChangeUpdatedValues.spec | 4 +- certora/specs/PostMaturityDebt.spec | 2 +- certora/specs/Ratification.spec | 2 +- certora/specs/Reverts.spec | 4 +- certora/specs/SettlementFeeBoundaries.spec | 6 ++- certora/specs/SettlementFeeSpread.spec | 4 +- certora/specs/Solvency.spec | 6 +-- .../SplitDoesNotPunishMakerOrFavorTaker.spec | 2 +- certora/specs/SplitPreservesAccounting.spec | 2 +- .../specs/TakeAmountsLibInvertibility.spec | 4 +- certora/specs/UpdateBeforeCredit.spec | 2 +- certora/specs/WithdrawableMonotonicity.spec | 8 +++- src/Midnight.sol | 20 ++++---- src/interfaces/IMidnight.sol | 5 +- src/libraries/IdLib.sol | 15 +++--- src/periphery/MidnightBundles.sol | 9 ++-- src/ratifiers/libraries/HashLib.sol | 48 ++++++++++--------- test/AuthorizationTest.sol | 2 + test/BaseTest.sol | 6 ++- 35 files changed, 112 insertions(+), 92 deletions(-) diff --git a/certora/specs/BalanceEffects.spec b/certora/specs/BalanceEffects.spec index 67fe2380a..e710509fd 100644 --- a/certora/specs/BalanceEffects.spec +++ b/certora/specs/BalanceEffects.spec @@ -12,7 +12,7 @@ methods { function continuousFeeCredit(bytes32 id) external returns (uint128) envfree; // Summarize internals irrelevant to credit and debt tracking. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/BundlerRepayInvertibility.spec b/certora/specs/BundlerRepayInvertibility.spec index e1ccd3ce6..7967e6ebf 100644 --- a/certora/specs/BundlerRepayInvertibility.spec +++ b/certora/specs/BundlerRepayInvertibility.spec @@ -10,7 +10,7 @@ methods { function midnight.tickSpacing(bytes32 id) external returns (uint8) envfree; // Deterministic market id (same pattern as Midnight.spec / TakeAmountsLibInvertibility.spec). - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic mulDivDown. function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivDown(x, y, d); diff --git a/certora/specs/CollateralBitmap.spec b/certora/specs/CollateralBitmap.spec index 82acdd8d4..a74cc8277 100644 --- a/certora/specs/CollateralBitmap.spec +++ b/certora/specs/CollateralBitmap.spec @@ -18,7 +18,7 @@ methods { */ function _.price() external => PER_CALLEE_CONSTANT; function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => NONDET; /* Simplify mulDiv reasoning for the solver. We summarize these by ghost functions, i.e., * arbitrary deterministic functions and axiomatize the axioms we need. diff --git a/certora/specs/Consume.spec b/certora/specs/Consume.spec index 5af605c5c..d15a14659 100644 --- a/certora/specs/Consume.spec +++ b/certora/specs/Consume.spec @@ -7,7 +7,7 @@ methods { function totalUnits(bytes32 id) external returns (uint128) envfree; // Summaries for complex internals irrelevant to consumed-mapping properties. - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; function UtilsLib.mulDivDown(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivUp(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/ContinuousFee.spec b/certora/specs/ContinuousFee.spec index 847f314de..d86e601cd 100644 --- a/certora/specs/ContinuousFee.spec +++ b/certora/specs/ContinuousFee.spec @@ -3,7 +3,7 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => CVL_toId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => CVL_toId(market); function credit(bytes32 id, address user) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; @@ -11,7 +11,7 @@ methods { function continuousFeeCredit(bytes32 id) external returns (uint128) envfree; // Summarize internals irrelevant to continuous fee tracking. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; diff --git a/certora/specs/CreatedMarkets.spec b/certora/specs/CreatedMarkets.spec index 62000bf77..8a5618e83 100644 --- a/certora/specs/CreatedMarkets.spec +++ b/certora/specs/CreatedMarkets.spec @@ -25,10 +25,10 @@ methods { function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => ghostMulDivDown(x, y, d); // Summary is required because abi.encodePacked doesn't ensure injectivity of the hash function in CVL, for an unknown reason. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound because the protocol doesn't use toMarket. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Tokens are assumed to not reenter, for performance reasons. function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; diff --git a/certora/specs/EmptyOffer.spec b/certora/specs/EmptyOffer.spec index 22309b982..77b2045d9 100644 --- a/certora/specs/EmptyOffer.spec +++ b/certora/specs/EmptyOffer.spec @@ -7,7 +7,7 @@ methods { function Utils.emptyOffer() external returns (Midnight.Offer) envfree; // Summarize internals, which is sound since it would only remove revert reasons. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/Healthiness.spec b/certora/specs/Healthiness.spec index bdf41b042..1c602ce65 100644 --- a/certora/specs/Healthiness.spec +++ b/certora/specs/Healthiness.spec @@ -16,7 +16,7 @@ methods { // Under this assumption we can prove that a healthy borrower cannot get unhealthy by any action on the contract. function _.price() external => summaryPrice(calledContract) expect(uint256); function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Summarize mulDivDown and mulDivUp to simplify the verification task. // Use a ghost function that ensures mulDivDown/Up behaves deterministically and add only the axioms about mulDiv that are needed to prove the desired property. @@ -25,7 +25,7 @@ methods { function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivUp(x, y, d); function _.havocAll() external => HAVOC_ALL; - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => transferCallback(); function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => transferCallback(); @@ -138,9 +138,9 @@ function getGlobalMarket() returns (Midnight.Market) { return market; } -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns (bytes32) { +function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && midnight == currentContract) { + if (equalsGlobalMarket(market) && market.midnight == currentContract) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; diff --git a/certora/specs/Liquidate.spec b/certora/specs/Liquidate.spec index 4ca0919c2..dcdce416a 100644 --- a/certora/specs/Liquidate.spec +++ b/certora/specs/Liquidate.spec @@ -18,14 +18,14 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => summaryMulDivUp(a, b, denominator); // IdLib summary: remember the last id returned by toId. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); } /// HELPERS /// persistent ghost bytes32 liqId; -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns bytes32 { +function summaryToId(Midnight.Market market) returns bytes32 { bytes32 id; liqId = id; return id; diff --git a/certora/specs/LiquidationBoundedByLIF.spec b/certora/specs/LiquidationBoundedByLIF.spec index efd665e99..5874e4cca 100644 --- a/certora/specs/LiquidationBoundedByLIF.spec +++ b/certora/specs/LiquidationBoundedByLIF.spec @@ -14,7 +14,7 @@ methods { function _.price() external => summaryPrice(calledContract) expect(uint256); // Deterministic toId summary using a wrapper that extracts all scalar Market fields. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Skip market creation logic: removes the collateral-validation loop. function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); diff --git a/certora/specs/LiquidationProfitability.spec b/certora/specs/LiquidationProfitability.spec index 220e1067d..52b8794a4 100644 --- a/certora/specs/LiquidationProfitability.spec +++ b/certora/specs/LiquidationProfitability.spec @@ -15,7 +15,7 @@ methods { function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivUp(x, y, d); // Deterministic toId summary using a wrapper that extracts all scalar Market fields. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Skip market creation logic: removes the collateral-validation loop. function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); diff --git a/certora/specs/LossFactor.spec b/certora/specs/LossFactor.spec index 4ac816dc5..beebf46be 100644 --- a/certora/specs/LossFactor.spec +++ b/certora/specs/LossFactor.spec @@ -14,8 +14,8 @@ methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Deterministic toId needed to link market arguments to stored state. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // SafeTransferLib summaries: bypass transfer logic (needed for liquidate @withrevert rules). function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/Midnight.spec b/certora/specs/Midnight.spec index a8731d849..fbe67720b 100644 --- a/certora/specs/Midnight.spec +++ b/certora/specs/Midnight.spec @@ -15,8 +15,8 @@ methods { function tickSpacing(bytes32 id) external returns (uint8) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; diff --git a/certora/specs/NoDivisionByZero.spec b/certora/specs/NoDivisionByZero.spec index 1eb8c1c5f..e6d972fc5 100644 --- a/certora/specs/NoDivisionByZero.spec +++ b/certora/specs/NoDivisionByZero.spec @@ -19,7 +19,7 @@ methods { function _.price() external => ghostPrice(calledContract) expect(uint256); // Summary for deterministic toId for the global market. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // This function is checked manually to not cause a division by zero. function TickLib.tickToPrice(uint256) internal returns (uint256) => NONDET; @@ -74,9 +74,9 @@ function equalsGlobalMarket(Midnight.Market market) returns (bool) { return market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; } -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns (bytes32) { +function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && midnight == currentContract) { + if (equalsGlobalMarket(market) && market.midnight == currentContract) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; diff --git a/certora/specs/NoMultiplicationOverflow.spec b/certora/specs/NoMultiplicationOverflow.spec index 51ba4fd21..b7ef4f146 100644 --- a/certora/specs/NoMultiplicationOverflow.spec +++ b/certora/specs/NoMultiplicationOverflow.spec @@ -14,7 +14,7 @@ methods { 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); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound return bound: tickToPrice <= WAD for non-reverting calls. function TickLib.tickToPrice(uint256) internal returns (uint256) => boundedTickPrice(); diff --git a/certora/specs/OnlyAuthorizedCanChange.spec b/certora/specs/OnlyAuthorizedCanChange.spec index 03b6ef455..bd802cb98 100644 --- a/certora/specs/OnlyAuthorizedCanChange.spec +++ b/certora/specs/OnlyAuthorizedCanChange.spec @@ -3,7 +3,6 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function toId(Midnight.Market market) external returns (bytes32) 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; @@ -11,7 +10,7 @@ methods { function isAuthorized(address authorizer, address authorized) external returns (bool) envfree; // Summarize internal functions that use opcodes causing HAVOC (CREATE2, low-level calls). - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; diff --git a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec index 22dc0aa47..a32cb5b3d 100644 --- a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec +++ b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec @@ -16,10 +16,10 @@ methods { function lossFactor(bytes32) external returns (uint128) envfree; // Summarize toId to be able to reference the id in the rules. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound because the protocol doesn't use toMarket. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; diff --git a/certora/specs/PostMaturityDebt.spec b/certora/specs/PostMaturityDebt.spec index ee689b83d..de6de2aa1 100644 --- a/certora/specs/PostMaturityDebt.spec +++ b/certora/specs/PostMaturityDebt.spec @@ -9,7 +9,7 @@ methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Deterministic toId summary. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Callbacks and token transfers reached by take/repay/liquidate/flashLoan use the default AUTO summary // (HAVOC_ECF), which assumes the callees do not re-enter Midnight and so leave the debt unchanged. diff --git a/certora/specs/Ratification.spec b/certora/specs/Ratification.spec index 6ec782598..cd02755e4 100644 --- a/certora/specs/Ratification.spec +++ b/certora/specs/Ratification.spec @@ -6,7 +6,7 @@ methods { function isAuthorized(address authorizer, address authorized) external returns (bool) envfree; // Over-approximate view functions. - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; function UtilsLib.mulDivDown(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivUp(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 2dfcccde1..952428e43 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -45,10 +45,10 @@ methods { function SafeTransferLib.safeTransfer(address, address, uint256) internal => CVL_safeTransfer(); // Bitmap operations (msb, clearBit, setBit) are provided by BitmapSummaries.spec. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // The function toMarket is not used by the protocol. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function TickLib.tickToPrice(uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivDown(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => CVL_mulDivDown(a, b, denominator); diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index f924bc177..77cc9c42a 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -8,13 +8,17 @@ methods { function settlementFee(bytes32 id, uint256 timeToMaturity) external returns (uint256) envfree; function feeSetter() external returns (address) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; - function toId(Midnight.Market) external returns (bytes32) envfree; + function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function Utils.maxSettlementFee(uint256 index) external returns (uint256) envfree; // Over-approximate view functions. function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } +function toId(env e, Midnight.Market market) returns bytes32 { + return Utils.hashMarket(market); +} + /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. definition breakpointTime(uint256 index) returns uint256 = index == 0 ? 0 : index == 1 ? 86400 : index == 2 ? 7 * 86400 : index == 3 ? 30 * 86400 : index == 4 ? 90 * 86400 : index == 5 ? 180 * 86400 : index == 6 ? 360 * 86400 : 0; diff --git a/certora/specs/SettlementFeeSpread.spec b/certora/specs/SettlementFeeSpread.spec index dba07ea3a..2eb7e8b5e 100644 --- a/certora/specs/SettlementFeeSpread.spec +++ b/certora/specs/SettlementFeeSpread.spec @@ -9,13 +9,13 @@ methods { function settlementFee(bytes32, uint256) external returns (uint256) envfree; // Summary is required because abi.encodePacked doesn't ensure injectivity of the hash function in CVL, for an unknown reason. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic TickLib.tickToPrice summary to be able to reference the price in the rules. function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => summaryTickToPrice(tick); // Sound summary since toMarket is not used by the protocol. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index 4202619aa..bd579e9b6 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -10,7 +10,7 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => CVL_mulDivUp(a, b, denominator); // Summarize toId, this adds no assumption but allows to retrieve the loan token from the market id. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => CVL_toId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => CVL_toId(market); // Summaries for complex internals irrelevant to token balance tracking. function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; @@ -75,9 +75,9 @@ ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; ghost hash(address, uint256, uint256, address) returns bytes32; -function CVL_toId(Midnight.Market market, uint256 chainId, address midnight) returns bytes32 { +function CVL_toId(Midnight.Market market) returns bytes32 { // Deterministically derive the market id. - bytes32 id = hash(market.loanToken, market.maturity, chainId, midnight); + bytes32 id = hash(market.loanToken, market.maturity, market.initialChainId, market.midnight); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. diff --git a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec index 859a7384a..3d3d993a5 100644 --- a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec +++ b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec @@ -13,7 +13,7 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 d) internal returns (uint256) => ghostMulDivUp(a, b, d); // Summarize toId: deterministic hash preserves market-to-id relationship without adding assumptions. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Assume that the markets are already created. function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); diff --git a/certora/specs/SplitPreservesAccounting.spec b/certora/specs/SplitPreservesAccounting.spec index ed2681548..8ed4783ca 100644 --- a/certora/specs/SplitPreservesAccounting.spec +++ b/certora/specs/SplitPreservesAccounting.spec @@ -18,7 +18,7 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 d) internal returns (uint256) => ghostMulDivUp(a, b, d); // Deterministic hash preserves market-to-id relationship without adding assumptions. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Assume that the markets are already created. function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); diff --git a/certora/specs/TakeAmountsLibInvertibility.spec b/certora/specs/TakeAmountsLibInvertibility.spec index b8599b3b8..4ee3105be 100644 --- a/certora/specs/TakeAmountsLibInvertibility.spec +++ b/certora/specs/TakeAmountsLibInvertibility.spec @@ -13,8 +13,8 @@ methods { function TakeAmountsLibHarness.buyerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); function TakeAmountsLibHarness.sellerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); - // Deterministic id: same Market => same id. Matches what take() and TakeAmountsLib observe (both reach IdLib.toId via Midnight.toId / Midnight.touchMarket). - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + // Deterministic id: same Market => same id. Matches what take() and TakeAmountsLib observe (both reach IdLib.toId via IdLib.toId / Midnight.touchMarket). + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic price: the same tick yields the same price for both the library and take(). function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => summaryTickToPrice(tick); diff --git a/certora/specs/UpdateBeforeCredit.spec b/certora/specs/UpdateBeforeCredit.spec index 323eeaf67..d5d7b1089 100644 --- a/certora/specs/UpdateBeforeCredit.spec +++ b/certora/specs/UpdateBeforeCredit.spec @@ -10,7 +10,7 @@ methods { function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; function UtilsLib.countBits(uint128) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; // Summarize _updatePosition so that its credit reads/writes do not fire the hooks below. function _updatePosition(Midnight.Market memory, bytes32 id, address user) internal returns (uint128, uint128, uint128) => summaryUpdatePosition(id, user); diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 046572958..c7c99e5a5 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -1,11 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; function withdrawable(bytes32 id) external returns (uint128) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; - function toId(Midnight.Market) external returns (bytes32); + function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; +} + +function toId(env e, Midnight.Market market) returns bytes32 { + return Utils.hashMarket(market); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { diff --git a/src/Midnight.sol b/src/Midnight.sol index 2f6fc6e7f..382f1d3a0 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,9 +179,9 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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, -/// so a hard fork that changes block.chainid does not strand existing accounting. But as a result, after a hard-fork -/// there can be some market id clashes. +/// @dev INITIAL_CHAIN_ID is captured at construction and must be embedded in every market together with this Midnight +/// address, so a hard fork that changes block.chainid does not strand existing accounting. But as a result, after a +/// hard-fork there can be some market id clashes. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). @@ -329,7 +329,7 @@ contract Midnight is IMidnight { } function claimContinuousFee(Market memory market, uint256 amount, address receiver) external { - bytes32 id = toId(market); + bytes32 id = IdLib.toId(market); MarketState storage _marketState = marketState[id]; require(msg.sender == feeClaimer, OnlyFeeClaimer()); require(_marketState.tickSpacing > 0, MarketNotCreated()); @@ -778,7 +778,9 @@ contract Midnight is IMidnight { /// @dev Returns the market id and creates the market if it doesn't exist yet. function touchMarket(Market memory market) public returns (bytes32) { - bytes32 id = toId(market); + require(market.initialChainId == INITIAL_CHAIN_ID, InvalidInitialChainId()); + require(market.midnight == address(this), InvalidMidnight()); + bytes32 id = IdLib.toId(market); if (marketState[id].tickSpacing == 0) { require(market.maturity <= block.timestamp + 100 * 365 days, MaturityTooFar()); require(market.collateralParams.length > 0, NoCollateralParams()); @@ -808,7 +810,7 @@ contract Midnight is IMidnight { _marketState.settlementFeeCbp5 = _defaultSettlementFeeCbp[5]; _marketState.settlementFeeCbp6 = _defaultSettlementFeeCbp[6]; _marketState.continuousFee = defaultContinuousFee[market.loanToken]; - IdLib.storeInCode(market, INITIAL_CHAIN_ID); + IdLib.storeInCode(market); emit EventsLib.MarketCreated(market, id); } @@ -846,7 +848,7 @@ contract Midnight is IMidnight { /// @dev Slashes the position and accrues the continuous fee. /// @dev Returns the new credit, new pending fee, and accrued fee after having updated the position. function updatePosition(Market memory market, address user) external returns (uint128, uint128, uint128) { - bytes32 id = toId(market); + bytes32 id = IdLib.toId(market); require(marketState[id].tickSpacing > 0, MarketNotCreated()); return _updatePosition(market, id, user); } @@ -909,10 +911,6 @@ contract Midnight is IMidnight { return position[id][user].collateral[index]; } - function toId(Market memory market) public view returns (bytes32) { - return IdLib.toId(market, INITIAL_CHAIN_ID, address(this)); - } - /// @dev Reverts if the id is not a valid id of a touched market. /// @dev Returns the market corresponding to the given id. function toMarket(bytes32 id) external view returns (Market memory) { diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 7d93054da..2c9a3636f 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -3,6 +3,8 @@ pragma solidity >=0.5.0; struct Market { + uint256 initialChainId; + address midnight; address loanToken; CollateralParams[] collateralParams; uint256 maturity; @@ -77,8 +79,10 @@ interface IMidnight { error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); error InvalidFeeIndex(); + error InvalidInitialChainId(); error InvalidLltv(); error InvalidMaxLif(); + error InvalidMidnight(); error InvalidOfferCaps(); error InvalidTickSpacing(); error LiquidatorGatedFromLiquidating(); @@ -171,7 +175,6 @@ interface IMidnight { function lastLossFactor(bytes32 id, address user) external view returns (uint128); function collateralBitmap(bytes32 id, address user) external view returns (uint128); 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 credit(bytes32 id, address user) external view returns (uint128); function debt(bytes32 id, address user) external view returns (uint128); diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index 85586d13f..eb757aa14 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -22,20 +22,23 @@ library IdLib { /// f3 RETURN [] mem[0:len] is returned bytes constant SSTORE2_PREFIX = hex"600b380380600b5f395ff3"; - function toId(Market memory market, uint256 chainId, address midnight) internal pure returns (bytes32) { + function toId(Market memory market) internal pure returns (bytes32) { return keccak256( abi.encodePacked( - uint8(0xff), midnight, chainId, keccak256(abi.encodePacked(SSTORE2_PREFIX, abi.encode(market))) + uint8(0xff), + market.midnight, + uint256(0), + keccak256(abi.encodePacked(SSTORE2_PREFIX, abi.encode(market))) ) ); } - /// @dev Stores the data in the code of the contract at the given address. - /// @dev Uses the given chain id as salt. - function storeInCode(Market memory market, uint256 chainId) internal returns (address create2Address) { + /// @dev Stores the market in the code of the contract at its id-derived address. + /// @dev Uses zero as salt because initialChainId and midnight are part of the encoded market. + function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { - create2Address := create2(0, add(creationCode, 0x20), mload(creationCode), chainId) + create2Address := create2(0, add(creationCode, 0x20), mload(creationCode), 0) } require(create2Address != address(0), SStore2DeploymentFailed()); } diff --git a/src/periphery/MidnightBundles.sol b/src/periphery/MidnightBundles.sol index 9e0823130..f2c574a6c 100644 --- a/src/periphery/MidnightBundles.sol +++ b/src/periphery/MidnightBundles.sol @@ -15,6 +15,7 @@ import { import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; import {IPermit2} from "./interfaces/IPermit2.sol"; import {UtilsLib} from "../libraries/UtilsLib.sol"; +import {IdLib} from "../libraries/IdLib.sol"; import {SafeTransferLib} from "../libraries/SafeTransferLib.sol"; import {TakeAmountsLib} from "./TakeAmountsLib.sol"; import {ConsumableUnitsLib} from "./ConsumableUnitsLib.sol"; @@ -80,7 +81,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledBuyerAssets; for (uint256 i; i < takes.length && filledUnits < targetUnits; i++) { require(!takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( targetUnits - filledUnits, takes[i].units, @@ -147,7 +148,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledSellerAssets; for (uint256 i; i < takes.length && filledUnits < targetUnits; i++) { require(takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( targetUnits - filledUnits, takes[i].units, @@ -204,7 +205,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledBuyerAssets; for (uint256 i; i < takes.length && filledBuyerAssets < targetFilledBuyerAssets; i++) { require(!takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( TakeAmountsLib.buyerAssetsToUnits( MIDNIGHT, id, takes[i].offer, targetFilledBuyerAssets - filledBuyerAssets @@ -275,7 +276,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledSellerAssets; for (uint256 i; i < takes.length && filledSellerAssets < targetFilledSellerAssets; i++) { require(takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( TakeAmountsLib.sellerAssetsToUnits( MIDNIGHT, id, takes[i].offer, targetFilledSellerAssets - filledSellerAssets diff --git a/src/ratifiers/libraries/HashLib.sol b/src/ratifiers/libraries/HashLib.sol index 13d8be011..24d89a74d 100644 --- a/src/ratifiers/libraries/HashLib.sol +++ b/src/ratifiers/libraries/HashLib.sol @@ -7,9 +7,9 @@ import {Offer, Market, CollateralParams} from "../../interfaces/IMidnight.sol"; /// @dev keccak256("CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"). bytes32 constant COLLATERAL_PARAMS_TYPEHASH = 0xaf44a88eb50ebdbbebd980e5a23045c44f61ece5f80ab708a1bbe8718102e6af; /// @dev keccak256(bytes.concat(MARKET_TYPE, COLLATERAL_PARAMS_TYPE)). -bytes32 constant MARKET_TYPEHASH = 0x358117e98511cc3df97175dca58053b06675b43ad090b0553f8a1eff008b6e2e; +bytes32 constant MARKET_TYPEHASH = 0x3fff861e0066633dc719c2511eb3c771b1517a7f8fad53b0bef7edd4a94d398d; /// @dev keccak256(bytes.concat(OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). -bytes32 constant OFFER_TYPEHASH = 0x6bd2a06ec6952feb97c3e3b4f7de6c342f12b1ac769d5c91368271af636c85b7; +bytes32 constant OFFER_TYPEHASH = 0x2d9b2c48e9f7b87e10d277cc9eb2abc4a9cd2ec3451bd3990be472fe4ac78137; library HashLib { error LeafIndexOutOfRange(); @@ -21,28 +21,28 @@ library HashLib { /// @dev Reverts if height is greater than 20. function offerTreeTypeHash(uint256 height) internal pure returns (bytes32) { if (height <= 10) { - if (height == 0) return 0xc27c38e446b48c820ab9c4373dc63a4a750a08165cb4bb488206ebabe045d650; - if (height == 1) return 0x4e15d8736f4406e07bf9844b1653474472a827130c61e899bf1f574a88b8d987; - if (height == 2) return 0x46d107447b480c38ef5b7f54603dba0cb23b887f302b01a998b9d8a80320dd53; - if (height == 3) return 0xd1f3607a8e81454bb3baf5f898274ab47541fffc690278a74f13e174e116be72; - if (height == 4) return 0xb2d98adca9d116c9bc02ce59ec599ac3c2d33db1c0d1217c7e411d9198d427be; - if (height == 5) return 0x5931e0597fcf986027f3118b2495a9ac22139d133f9ad2c2198e6738dc3886c5; - if (height == 6) return 0x3967d37928614a085b47e8758fbc3869a8aed63bdf60ccee8536ff2b5064da06; - if (height == 7) return 0xd6b9f5f45915a260f6e521d9b40f86c385730b6bc330590fcde212e2fea64263; - if (height == 8) return 0x080caa519dbd5328c119d9907e0fa3d9a50dc2ae4bf6dd42c93c100dbc89b51a; - if (height == 9) return 0x45da471048924165ea2ad1855ba940e454b486e71dcf1666c71a928c8844c419; - return 0xa49a9434fc1836bd08097368325b31039b6a0fd44919f53e4d8f4bf814084cb0; + if (height == 0) return 0xb7013fa5a999e5458627829111d031bbfa33e47889dca4e3514af04af509f09f; + if (height == 1) return 0xf815228253919597971ee6a6ca2adf2c3d8fc6296c7b86836ef214bd443daa58; + if (height == 2) return 0x893585f92cecb9f687cb75f92a57a6fc1754d8e5dad457ae1788158b26431712; + if (height == 3) return 0x86a3d14f6eb5735e5b94524e19d4c3d93ac15ac07ff41cc0a56ac928c6196b75; + if (height == 4) return 0xab8a93d114b72108b54341384805d18d28645e84d0c529c2c51953458047059f; + if (height == 5) return 0xb17a31924a5fc4d78bd61198e129de2fbdb46b962ed5c31ede6e96387491219f; + if (height == 6) return 0xbde56d2c07e3653be3d1f612a6f80ee60b75b327b5f2de0ebee98edea7b691db; + if (height == 7) return 0x6b2caaf1294590779ca6a569e9568e6980c547581cffff04f04a010e4bea0181; + if (height == 8) return 0x73b960155d0c9f339157d36610ccd76774a8dcd8a613f2127880e6d1f3590c5a; + if (height == 9) return 0xcc0480fc3e9396c4ecc52012adf81e481e0d1edecc3933d3b23e8150d26fd8e3; + return 0xada0eb530ea41beaf0af9972929d04e52040aaec090096b0598c39a34167bab6; } else { - if (height == 11) return 0xd3e93e4525132f0187a6964dc01fef33fde414538ffd212e9f2f478c3263e0a0; - if (height == 12) return 0x25990db2d26547f92c711988300df317af57bad5cd5d9d8e787a82f95c929474; - if (height == 13) return 0x8e0c648afa977572ead40a1d10a6db2c425b8099545006d834a7b849c6166643; - if (height == 14) return 0x4b635250efa6243e277fdd0cf6df993c2943b64f10f3a0756ceb1f47ef8f9b18; - if (height == 15) return 0xbde1c927f6222c07c8df264e68b42b8382c7c2b85f4729e0df94297cfeebfa91; - if (height == 16) return 0x4d58aea1a67f94be21ab1415bf3b602592430eb9112268fd0fc4e141b1a35e76; - if (height == 17) return 0x14c03281bce13010b158e5a4a3378be394ac9e16118aedb17d82ced51e66836c; - if (height == 18) return 0x99fd3e76f43b2cc221cb9860bc6c96cda95af3fa07ef5f04e071b54aa9386d06; - if (height == 19) return 0x1b1c2f1a04968094d8d0453d49838f7a809d1202ae04a1c2e0964e442ff7988b; - if (height == 20) return 0xc8ccd3cb3267dd76f563584920ac60f2283b719917481b85fe5e10b754932455; + if (height == 11) return 0x81fa0e3280af655d1107469f0cd022c73f57a8b97b0ac19757162e4ccf21b6da; + if (height == 12) return 0x16f061da90a7f30a2af2f5ccc13be13a022de02a32aee285be67085861b8b913; + if (height == 13) return 0x7195f81e3204ac6040f29531f347d18dbe10552cd4618aba0793abb71cc6c19c; + if (height == 14) return 0x48659bfedd6ab7bbdaa4a0fb2491b9150191c54f84cdc6da5d2a4baea9f2722f; + if (height == 15) return 0x3f18ed4b2ad3720c79905889f1900cb770043edab868f1afa0d4cca5b7623773; + if (height == 16) return 0x3f939e01857b0725e963c412a35e5ee75cbc8c51d40cc5b33c11a4ef275b0086; + if (height == 17) return 0x990e2f81f815d7e02dc7cd5bc6aa2ea6631372ce9563a586dc60807107a7320c; + if (height == 18) return 0xf567d558ad0daa6e76e75e5bcba9d858ffcc43c1e48a98b07ce7665f41003158; + if (height == 19) return 0x957d376d258c67c3c69b7a67ac562821c7a2c5e9c189e19e8a041ba5e4a2e223; + if (height == 20) return 0x72e1d8247b3df82dfe2c47a385bba604682f719edf060edccd3a3e5c7e402849; revert TreeTooHigh(); } } @@ -103,6 +103,8 @@ library HashLib { return keccak256( abi.encode( MARKET_TYPEHASH, + market.initialChainId, + market.midnight, market.loanToken, collateralParamsHash, market.maturity, diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index f901bdab5..93f5ecc40 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -19,6 +19,8 @@ contract AuthorizationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/BaseTest.sol b/test/BaseTest.sol index a662137ee..947ff016e 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -244,8 +244,8 @@ abstract contract BaseTest is Test { Oracle(market.collateralParams[0].oracle).setPrice(ORACLE_PRICE_SCALE); } - function toId(Market memory market) internal view returns (bytes32) { - return IdLib.toId(market, block.chainid, address(midnight)); + function toId(Market memory market) internal pure returns (bytes32) { + return IdLib.toId(market); } function domainSeparator(address verifyingContract) internal view returns (bytes32) { @@ -297,6 +297,8 @@ abstract contract BaseTest is Test { collateralParams[i].maxLif = maxLif(lltv, LIQUIDATION_CURSOR_LOW); } collateralParams = sortCollateralParams(collateralParams); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.collateralParams = collateralParams; market.maturity = bound(market.maturity, 0, vm.getBlockTimestamp() + 100 * 365 days); return market; From a0073921a439af8ed744b1dff6d9a1f1f57e6c21 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:10:55 +0000 Subject: [PATCH 02/29] chore: update market id references 2/3 --- test/ContinuousFeeTest.sol | 2 + test/EcrecoverRatifierIntegrationTest.sol | 2 + test/FrontendSignatureTest.sol | 8 +- test/GateTest.sol | 4 + test/HashLibTest.sol | 4 +- test/IdLibTest.sol | 98 +++++++++++------------ test/LiquidationTest.sol | 4 +- test/MaxAmountsTest.sol | 2 + test/MidnightBundlesTest.sol | 2 + test/OtherFunctionsTest.sol | 52 +++++++++++- test/SetterRatifierTest.sol | 2 + test/SettersTest.sol | 12 +++ test/SettlementFeeTest.sol | 2 + test/TakeAmountsTest.sol | 2 + 14 files changed, 137 insertions(+), 59 deletions(-) diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index 3507e525a..cd6a48fcb 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -23,6 +23,8 @@ contract ContinuousFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100 days; market.collateralParams .push( diff --git a/test/EcrecoverRatifierIntegrationTest.sol b/test/EcrecoverRatifierIntegrationTest.sol index 0c1776d72..a4b6138b4 100644 --- a/test/EcrecoverRatifierIntegrationTest.sol +++ b/test/EcrecoverRatifierIntegrationTest.sol @@ -25,6 +25,8 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/FrontendSignatureTest.sol b/test/FrontendSignatureTest.sol index df675cbe5..1643d47ae 100644 --- a/test/FrontendSignatureTest.sol +++ b/test/FrontendSignatureTest.sol @@ -9,10 +9,10 @@ import {CALLBACK_SUCCESS} from "../src/libraries/ConstantsLib.sol"; import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; // Paste from frontend output. -address constant ACCOUNT = 0xFDa6883171208B36122229505FB2D6F30c052311; +address constant ACCOUNT = 0x6133Fd1B38C7D9ad120a6fC0D0f7b03d4F6E9658; uint8 constant SIG_V = 28; -bytes32 constant SIG_R = 0xb7a8c34b3aa87d799f2bd6e01a36c6a48673313015c592fc4137043b37ee80c6; -bytes32 constant SIG_S = 0x5161ce684b17e81a0b297441856e8d8b498c541116fd6dde5d87e5624b847afb; +bytes32 constant SIG_R = 0xbdd5b0f61aafd17bcae94af84e87a9d7d80c3a54eb4e6ffb0b5efc78da812a01; +bytes32 constant SIG_S = 0x7b05c5581f10b0a79ac1bd89dfbf226e1c47921b9d68cfba47e5cd98d16a4c63; address constant RATIFIER = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; @@ -25,6 +25,8 @@ contract FrontendSignatureTest is Test { function defaultOffer(uint8 number) internal pure returns (Offer memory offer) { CollateralParams[] memory collateralParams = new CollateralParams[](1); + offer.market.initialChainId = 1; + offer.market.midnight = address(0); offer.market.loanToken = address(uint160(0x1111111111111111111111111111111111111111) * uint160(number)); offer.market.collateralParams = collateralParams; offer.expiry = 2 ** 32; diff --git a/test/GateTest.sol b/test/GateTest.sol index f587e8125..59d812b4f 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -43,6 +43,8 @@ contract GateTest is BaseTest { gate = new WhitelistGate(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -56,6 +58,8 @@ contract GateTest is BaseTest { market.collateralParams = sortCollateralParams(market.collateralParams); gatedMarket.loanToken = address(loanToken); + gatedMarket.initialChainId = midnight.INITIAL_CHAIN_ID(); + gatedMarket.midnight = address(midnight); gatedMarket.maturity = vm.getBlockTimestamp() + 100; gatedMarket.collateralParams .push( diff --git a/test/HashLibTest.sol b/test/HashLibTest.sol index 42f5b5e9b..586e64bf3 100644 --- a/test/HashLibTest.sol +++ b/test/HashLibTest.sol @@ -12,7 +12,7 @@ import {Market} from "../src/interfaces/IMidnight.sol"; bytes constant COLLATERAL_PARAMS_TYPE = "CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"; bytes constant MARKET_TYPE = - "Market(address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; + "Market(uint256 initialChainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; bytes constant OFFER_TYPE = "Offer(Market market,bool buy,address maker,uint256 start,uint256 expiry,uint256 tick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,address ratifier,bool reduceOnly,uint256 maxUnits,uint256 maxAssets,uint256 continuousFeeCap)"; @@ -37,6 +37,8 @@ contract HashLibTest is Test { bytes32 expectedHash = keccak256( abi.encode( MARKET_TYPEHASH, + market.initialChainId, + market.midnight, market.loanToken, keccak256(abi.encodePacked(collateralParamsHashes)), market.maturity, diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index 01d59ad7c..2ad2d53dd 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -3,63 +3,61 @@ pragma solidity ^0.8.0; import {Test} from "../lib/forge-std/src/Test.sol"; import {IdLib} from "../src/libraries/IdLib.sol"; -import {Market} from "../src/interfaces/IMidnight.sol"; +import {Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; // toMarket is tested in OtherFunctionsTest.sol, to test actual implementation (avoid introducing mocks). contract IdLibTest is Test { - function testToIdIsInjectiveInMarket( - Market memory market1, - Market memory market2, - uint256 chainid, - address midnight - ) public pure { - bool sameLoanToken = market1.loanToken == market2.loanToken; - bool sameMaturity = market1.maturity == market2.maturity; - bool sameCollaterals = market1.collateralParams.length == market2.collateralParams.length; - bool sameRcfThreshold = market1.rcfThreshold == market2.rcfThreshold; - if (sameCollaterals) { - for (uint256 i = 0; i < market1.collateralParams.length; i++) { - if (market1.collateralParams[i].token != market2.collateralParams[i].token) { - sameCollaterals = false; - } - if (market1.collateralParams[i].lltv != market2.collateralParams[i].lltv) { - sameCollaterals = false; - } - if (market1.collateralParams[i].maxLif != market2.collateralParams[i].maxLif) { - sameCollaterals = false; - } - if (market1.collateralParams[i].oracle != market2.collateralParams[i].oracle) { - sameCollaterals = false; - } - } - } - - vm.assume(!(sameLoanToken && sameMaturity && sameCollaterals && sameRcfThreshold)); + function baseMarket() internal pure returns (Market memory market) { + market.initialChainId = 1; + market.midnight = address(1); + market.loanToken = address(2); + market.collateralParams = new CollateralParams[](1); + market.collateralParams[0] = + CollateralParams({token: address(3), lltv: 0.77e18, maxLif: 1.1e18, oracle: address(4)}); + market.maturity = 123; + market.rcfThreshold = 456; + market.enterGate = address(5); + market.liquidatorGate = address(6); + } - bytes32 id1 = IdLib.toId(market1, chainid, midnight); - bytes32 id2 = IdLib.toId(market2, chainid, midnight); - assertNotEq(id1, id2); + function testToIdIsSensitiveToInitialChainId() public pure { + Market memory market1 = baseMarket(); + Market memory market2 = baseMarket(); + market2.initialChainId = 2; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); } - function testToIdIsInjectiveInChainId(Market memory market, uint256 chainid1, uint256 chainid2, address midnight) - public - pure - { - vm.assume(chainid1 != chainid2); - bytes32 id1 = IdLib.toId(market, chainid1, midnight); - bytes32 id2 = IdLib.toId(market, chainid2, midnight); - assertNotEq(id1, id2); + function testToIdIsSensitiveToMidnight() public pure { + Market memory market1 = baseMarket(); + Market memory market2 = baseMarket(); + market2.midnight = address(7); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); } - function testToIdIsInjectiveInMidnight( - Market memory market, - uint256 chainid, - address midnightOne, - address midnightTwo - ) public pure { - vm.assume(midnightOne != midnightTwo); - bytes32 id1 = IdLib.toId(market, chainid, midnightOne); - bytes32 id2 = IdLib.toId(market, chainid, midnightTwo); - assertNotEq(id1, id2); + function testToIdIsSensitiveToMarketParams() public pure { + Market memory market1 = baseMarket(); + Market memory market2 = baseMarket(); + market2.loanToken = address(8); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].token = address(9); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.maturity = 789; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.rcfThreshold = 789; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.enterGate = address(10); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.liquidatorGate = address(11); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); } } diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 330dc54e0..e890443f7 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -45,6 +45,8 @@ contract LiquidationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -1014,7 +1016,7 @@ contract LiquidationTest is BaseTest { bytes memory data, uint256 badDebt ) public returns (bytes32) { - require(_id == IdLib.toId(_market, block.chainid, msg.sender), "wrong id"); + require(_id == IdLib.toId(_market), "wrong id"); recordedCaller = _caller; recordedId = _id; recordedMarket = _market; diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index fe1cefae8..079307508 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -20,6 +20,8 @@ contract MaxAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index 47f2a8443..2abde03fe 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -46,6 +46,8 @@ contract MidnightBundlesTest is BaseTest { } market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 08827607d..cb741c364 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -43,6 +43,8 @@ contract OtherFunctionsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -310,6 +312,8 @@ contract OtherFunctionsTest is BaseTest { bytes32 _id = midnight.touchMarket(_market); Market memory marketFromId = midnight.toMarket(_id); + assertEq(_market.initialChainId, marketFromId.initialChainId, "initialChainId"); + assertEq(_market.midnight, marketFromId.midnight, "midnight"); assertEq(_market.loanToken, marketFromId.loanToken, "loanToken"); assertEq(_market.maturity, marketFromId.maturity, "maturity"); assertEq(_market.collateralParams.length, marketFromId.collateralParams.length, "collateralParams length"); @@ -321,11 +325,11 @@ contract OtherFunctionsTest is BaseTest { } } - function testToId(Market memory _market) public view { + function testIdLibToId(Market memory _market) public view { _market = validMarket(_market); bytes32 expected = toId(_market); - bytes32 actual = midnight.toId(_market); + bytes32 actual = IdLib.toId(_market); assertEq(actual, expected, "toId mismatch"); } @@ -340,7 +344,7 @@ contract OtherFunctionsTest is BaseTest { vm.chainId(newChainId); assertEq(midnight.INITIAL_CHAIN_ID(), capturedChainId, "INITIAL_CHAIN_ID changed"); - assertEq(midnight.toId(_market), idBefore, "toId changed"); + assertEq(toId(_market), idBefore, "toId changed"); Market memory roundTrip = midnight.toMarket(idBefore); assertEq(keccak256(abi.encode(roundTrip)), keccak256(abi.encode(_market)), "stored market lost"); @@ -355,6 +359,24 @@ contract OtherFunctionsTest is BaseTest { midnight.toMarket(_id); } + function testTouchMarketInvalidInitialChainId(Market memory _market) public { + vm.assume(_market.collateralParams.length > 0); + _market = validMarket(_market); + _market.initialChainId = midnight.INITIAL_CHAIN_ID() + 1; + + vm.expectRevert(IMidnight.InvalidInitialChainId.selector); + midnight.touchMarket(_market); + } + + function testTouchMarketInvalidMidnight(Market memory _market) public { + vm.assume(_market.collateralParams.length > 0); + _market = validMarket(_market); + _market.midnight = address(0); + + vm.expectRevert(IMidnight.InvalidMidnight.selector); + midnight.touchMarket(_market); + } + function testSstore2CodeStartsWithStop(Market memory _market) public { vm.assume(_market.collateralParams.length > 0); _market = validMarket(_market); @@ -379,6 +401,8 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.initialChainId = midnight.INITIAL_CHAIN_ID(); + marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -403,6 +427,8 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.initialChainId = midnight.INITIAL_CHAIN_ID(); + marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -431,6 +457,8 @@ contract OtherFunctionsTest is BaseTest { } collateralParams = sortCollateralParams(collateralParams); _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = collateralParams; _market.rcfThreshold = 0; @@ -440,6 +468,8 @@ contract OtherFunctionsTest is BaseTest { maturity = bound(maturity, vm.getBlockTimestamp() + 100 * 365 days + 1, type(uint256).max); Market memory longMarket; longMarket.loanToken = address(loanToken); + longMarket.initialChainId = midnight.INITIAL_CHAIN_ID(); + longMarket.midnight = address(midnight); longMarket.maturity = maturity; longMarket.collateralParams = market.collateralParams; @@ -450,6 +480,8 @@ contract OtherFunctionsTest is BaseTest { function testZeroCollaterals() public { Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = new CollateralParams[](0); vm.expectRevert(IMidnight.NoCollateralParams.selector); @@ -480,6 +512,8 @@ contract OtherFunctionsTest is BaseTest { function testCollateralsNotSorted() public { Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](2); collateralParams[0] = CollateralParams({ @@ -497,6 +531,8 @@ contract OtherFunctionsTest is BaseTest { lltv = bound(lltv, WAD + 1, type(uint256).max); Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -512,6 +548,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.5e18; Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -648,6 +686,8 @@ contract OtherFunctionsTest is BaseTest { Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = @@ -662,6 +702,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -677,6 +719,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.initialChainId = midnight.INITIAL_CHAIN_ID(); + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 200; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -781,7 +825,7 @@ contract RepayCallback { external returns (bytes32) { - require(marketId == IdLib.toId(market, block.chainid, msg.sender), "wrong marketId"); + require(marketId == IdLib.toId(market), "wrong marketId"); recordedId = marketId; _recordedMarket = market; recordedData = data; diff --git a/test/SetterRatifierTest.sol b/test/SetterRatifierTest.sol index b374aef86..8873d78c6 100644 --- a/test/SetterRatifierTest.sol +++ b/test/SetterRatifierTest.sol @@ -21,6 +21,8 @@ contract SetterRatifierTest is BaseTest { function makeOffer(address maker) internal view returns (Offer memory offer) { Market memory market; market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams = new CollateralParams[](1); market.collateralParams[0] = CollateralParams({ diff --git a/test/SettersTest.sol b/test/SettersTest.sol index bcd8ac21a..f3a6a8e93 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -125,6 +125,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, collateralParams: collateralParams, @@ -299,6 +301,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, collateralParams: collateralParams, @@ -356,6 +360,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: address(0), maturity: vm.getBlockTimestamp() + 1 days, collateralParams: cols, @@ -410,6 +416,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, @@ -437,6 +445,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, @@ -470,6 +480,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + initialChainId: midnight.INITIAL_CHAIN_ID(), + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index b1f32e399..558c3050c 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -38,6 +38,8 @@ contract SettlementFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); // to be able to come back in time enough market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 1 days; // TTM = 1 day (exactly at breakpoint) market.collateralParams .push( diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index d69a8b1b1..653a712fe 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -22,6 +22,8 @@ contract TakeAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( From e166d57c9932a916b464b7497505d67e479e7db4 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:10:57 +0000 Subject: [PATCH 03/29] chore: update market id references 3/3 --- test/TakeTest.sol | 10 ++++++---- test/TickGatingTest.sol | 2 ++ test/frontend/sign-root.ts | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/test/TakeTest.sol b/test/TakeTest.sol index 3d9abbc40..83ae0bae7 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -35,6 +35,8 @@ contract TakeTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -1512,7 +1514,7 @@ contract BorrowCallback is ISellCallback { address receiver, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); recordedId = id; _recordedMarket = market; recordedSeller = seller; @@ -1547,7 +1549,7 @@ contract ReentrantLiquidateBorrowCallback is ISellCallback { address, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); (uint256 collateralIndex, uint256 collateralAmount, uint256 repaidUnits) = abi.decode(data, (uint256, uint256, uint256)); address collateralToken = market.collateralParams[collateralIndex].token; @@ -1604,7 +1606,7 @@ contract NestedTakeReentrantLiquidateCallback is ISellCallback { external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); if (!reentered) { uint256 idx = storedCollateralIndex; address collateralToken = market.collateralParams[idx].token; @@ -1653,7 +1655,7 @@ contract LendCallback is IBuyCallback { address buyer, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); recordedId = id; _recordedMarket = market; recordedBuyer = buyer; diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index 015d1405a..cdab9fca4 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -20,6 +20,8 @@ contract TickGatingTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.initialChainId = midnight.INITIAL_CHAIN_ID(); + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/frontend/sign-root.ts b/test/frontend/sign-root.ts index 5235bed28..fa71a1e20 100644 --- a/test/frontend/sign-root.ts +++ b/test/frontend/sign-root.ts @@ -32,6 +32,8 @@ function buildTypes(height: number) { { name: "oracle", type: "address" }, ], Market: [ + { name: "initialChainId", type: "uint256" }, + { name: "midnight", type: "address" }, { name: "loanToken", type: "address" }, { name: "collateralParams", type: "CollateralParams[]" }, { name: "maturity", type: "uint256" }, @@ -62,6 +64,8 @@ function buildTypes(height: number) { function defaultOffer(number: string) { return { market: { + initialChainId: "1", + midnight: ZERO_ADDR, loanToken: "0x" + number.repeat(40), collateralParams: [{token: ZERO_ADDR, lltv: "0", maxLif: "0", oracle: ZERO_ADDR}], maturity: "0", From 1edd50db4212e6256fda0f7bdbf93feffe29355d Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:23:50 +0000 Subject: [PATCH 04/29] fix: align certora toId summaries Requested by: <@U02NB58F6R3> --- certora/specs/BalanceEffects.spec | 22 +++++++++-------- certora/specs/Role.spec | 19 ++++++++++++++- certora/specs/SettlementFeeBoundaries.spec | 20 ++++++++++++--- certora/specs/WithdrawableMonotonicity.spec | 27 ++++++++++++++------- 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/certora/specs/BalanceEffects.spec b/certora/specs/BalanceEffects.spec index e710509fd..fa383bd7a 100644 --- a/certora/specs/BalanceEffects.spec +++ b/certora/specs/BalanceEffects.spec @@ -3,6 +3,8 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + 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; @@ -34,7 +36,7 @@ methods { /// sets it to the post-update value, only changes credit of user at the market id, /// and accrues fee to continuousFeeCredit. rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 creditBefore = credit(id, user); uint128 updatedUserCredit; @@ -61,7 +63,7 @@ rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 /// withdraw decreases onBehalf's post-update credit by exactly units /// and only changes credit of onBehalf at the market id. rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address receiver, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint128 updatedUserCredit; uint128 userFee; @@ -84,7 +86,7 @@ rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBeh /// take changes maker's and taker's net credit-debt by +/- units relative to their post-update values /// and only changes credit of maker and taker and debt of maker and taker at the market id. rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, offer.market); + bytes32 id = summaryToId(offer.market); uint128 makerCreditBefore; makerCreditBefore, _, _ = updatePositionView(e, offer.market, id, offer.maker); @@ -112,7 +114,7 @@ rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, /// Buyer's credit is non-decreasing relative to its post-update value and can increase by at most take units. /// Buyer's debt is non-increasing and can decrease by at most take units. rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = toId(e, offer.market); + bytes32 id = summaryToId(offer.market); address buyer = offer.buy ? offer.maker : taker; uint256 buyerDebtBefore = debt(id, buyer); @@ -132,7 +134,7 @@ rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 u /// Seller's debt is non-decreasing, and can increase by at most take units. /// Seller's credit is non-increasing relative to its post-update value and can decrease by at most take units. rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = toId(e, offer.market); + bytes32 id = summaryToId(offer.market); address seller = offer.buy ? taker : offer.maker; uint256 sellerDebtBefore = debt(id, seller); @@ -152,7 +154,7 @@ rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 /// Repay decreases onBehalf's debt by exactly units and only changes position[id][onBehalf].debt rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 debtBefore = debt(id, onBehalf); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -170,7 +172,7 @@ rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf /// Liquidate decreases the borrower's debt by at least repaidUnits, /// and only changes position[id][borrower].debt. 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); + bytes32 id = summaryToId(market); uint256 debtBefore = debt(id, borrower); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -209,7 +211,7 @@ filtered { /// supplyCollateral increases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -225,7 +227,7 @@ rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIn /// withdrawCollateral decreases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, address receiver, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -241,7 +243,7 @@ rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 /// liquidate decreases the borrower's collateral at collateralIndex by exactly seizedResult, /// and only changes position[id][borrower].collateral[collateralIndex]. rule liquidateCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bytes32 anyId, address anyUser, uint256 anyIndex, bool postMaturityMode) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 collateralBefore = collateral(id, borrower, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index a8206c670..f59e38e69 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -5,6 +5,8 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function roleSetter() external returns (address) envfree; function feeSetter() external returns (address) envfree; function feeClaimer() external returns (address) envfree; @@ -27,6 +29,21 @@ methods { /// HELPERS /// +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; + +function summaryToId(Midnight.Market market) returns bytes32 { + return marketHash( + market.initialChainId, + market.midnight, + market.loanToken, + market.maturity, + market.rcfThreshold, + market.enterGate, + market.liquidatorGate + ); +} + + definition WAD() returns uint256 = 10 ^ 18; definition CBP() returns uint256 = 10 ^ 12; @@ -270,7 +287,7 @@ rule feeClaimerCanClaimSettlementFee(env e, address token, uint256 amount, addre } rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver, address user) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); address feeClaimerBefore = feeClaimer(); bool marketIsCreated = marketIsCreated(id); uint256 withdrawableBefore = withdrawable(id); diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index 77cc9c42a..f9df0cac7 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -5,18 +5,30 @@ using Utils as Utils; methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function settlementFee(bytes32 id, uint256 timeToMaturity) external returns (uint256) envfree; function feeSetter() external returns (address) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; - function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function Utils.maxSettlementFee(uint256 index) external returns (uint256) envfree; // Over-approximate view functions. function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } -function toId(env e, Midnight.Market market) returns bytes32 { - return Utils.hashMarket(market); + +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; + +function summaryToId(Midnight.Market market) returns bytes32 { + return marketHash( + market.initialChainId, + market.midnight, + market.loanToken, + market.maturity, + market.rcfThreshold, + market.enterGate, + market.liquidatorGate + ); } /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. @@ -70,7 +82,7 @@ invariant marketSettlementFeePerIndexBound(bytes32 id, uint256 index) /// When a market is created, its settlement fees are set to the default settlement fees of its loan token. rule newMarketSettlementFeesMatchDefault(env e, Midnight.Market market, uint256 index) { require index <= 6, "index out of bounds"; - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); require tickSpacing(id) == 0, "market not yet created"; uint256 expectedSettlementFee = defaultSettlementFee(market.loanToken, index); diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index c7c99e5a5..48a6d5039 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -1,21 +1,30 @@ // SPDX-License-Identifier: GPL-2.0-or-later -using Utils as Utils; - methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function withdrawable(bytes32 id) external returns (uint128) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; - function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; } -function toId(env e, Midnight.Market market) returns bytes32 { - return Utils.hashMarket(market); +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; + +function summaryToId(Midnight.Market market) returns bytes32 { + return marketHash( + market.initialChainId, + market.midnight, + market.loanToken, + market.maturity, + market.rcfThreshold, + market.enterGate, + market.liquidatorGate + ); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 withdrawableBefore = withdrawable(id); repay(e, market, units, onBehalf, callback, data); uint256 withdrawableAfter = withdrawable(id); @@ -23,7 +32,7 @@ rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, ad } rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 withdrawableBefore = withdrawable(id); uint256 seizedResult; uint256 repaidResult; @@ -33,7 +42,7 @@ rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 colla } rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 unitsInput, address onBehalf, address receiver) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 withdrawableBefore = withdrawable(id); withdraw(e, market, unitsInput, onBehalf, receiver); uint256 withdrawableAfter = withdrawable(id); @@ -41,7 +50,7 @@ rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 } rule claimContinuousFeeDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 amount, address receiver) { - bytes32 id = toId(e, market); + bytes32 id = summaryToId(market); uint256 withdrawableBefore = withdrawable(id); claimContinuousFee(e, market, amount, receiver); uint256 withdrawableAfter = withdrawable(id); From c20dfd49ec4eb20e0e2306273fc802b0b631ba84 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:25:57 +0000 Subject: [PATCH 05/29] style: format certora summaries Requested by: <@U02NB58F6R3> --- certora/specs/Role.spec | 11 +---------- certora/specs/SettlementFeeBoundaries.spec | 10 +--------- certora/specs/WithdrawableMonotonicity.spec | 10 +--------- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index f59e38e69..55421ea20 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -32,18 +32,9 @@ methods { ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash( - market.initialChainId, - market.midnight, - market.loanToken, - market.maturity, - market.rcfThreshold, - market.enterGate, - market.liquidatorGate - ); + return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } - definition WAD() returns uint256 = 10 ^ 18; definition CBP() returns uint256 = 10 ^ 12; diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index f9df0cac7..d42b9a0a9 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -20,15 +20,7 @@ methods { ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash( - market.initialChainId, - market.midnight, - market.loanToken, - market.maturity, - market.rcfThreshold, - market.enterGate, - market.liquidatorGate - ); + return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 48a6d5039..e68a3dc54 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -12,15 +12,7 @@ methods { ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash( - market.initialChainId, - market.midnight, - market.loanToken, - market.maturity, - market.rcfThreshold, - market.enterGate, - market.liquidatorGate - ); + return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { From 8636bbe4075d4e004380e96ad1d9d01b00cf24ca Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:28:31 +0000 Subject: [PATCH 06/29] style: fix certora formatter nit Requested by: <@U02NB58F6R3> --- certora/specs/SettlementFeeBoundaries.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index d42b9a0a9..63ada15e5 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -16,7 +16,6 @@ methods { function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } - ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { From dfae199793e0b26e392734fcaed0e9884bc25fd4 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:42:22 +0000 Subject: [PATCH 07/29] refactor: remove market initial chain id Requested by: <@U02NB58F6R3> --- certora/specs/Role.spec | 4 +- certora/specs/SettlementFeeBoundaries.spec | 4 +- certora/specs/Solvency.spec | 4 +- certora/specs/WithdrawableMonotonicity.spec | 4 +- src/Midnight.sol | 13 +----- src/interfaces/IMidnight.sol | 5 --- src/libraries/EventsLib.sol | 2 +- src/libraries/IdLib.sol | 2 +- src/ratifiers/libraries/HashLib.sol | 47 ++++++++++----------- test/AuthorizationTest.sol | 1 - test/BaseTest.sol | 1 - test/ContinuousFeeTest.sol | 1 - test/EcrecoverRatifierIntegrationTest.sol | 1 - test/FrontendSignatureTest.sol | 7 ++- test/GateTest.sol | 2 - test/HashLibTest.sol | 3 +- test/IdLibTest.sol | 8 ---- test/LiquidationTest.sol | 1 - test/MaxAmountsTest.sol | 1 - test/MidnightBundlesTest.sol | 1 - test/OtherFunctionsTest.sol | 25 ----------- test/SetterRatifierTest.sol | 1 - test/SettersTest.sol | 8 +--- test/SettlementFeeTest.sol | 1 - test/TakeAmountsTest.sol | 1 - test/TakeTest.sol | 1 - test/TickGatingTest.sol | 1 - test/frontend/sign-root.ts | 2 - 28 files changed, 40 insertions(+), 112 deletions(-) diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index 55421ea20..2cde8c70e 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -29,10 +29,10 @@ methods { /// HELPERS /// -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } definition WAD() returns uint256 = 10 ^ 18; diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index 63ada15e5..9b0e4b1cb 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -16,10 +16,10 @@ methods { function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index bd579e9b6..3eb902747 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -73,11 +73,11 @@ ghost mapping(bytes32 => address) loantoken; // Mapping from market id and collateral index to the corresponding collateral token. ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; -ghost hash(address, uint256, uint256, address) returns bytes32; +ghost hash(address, uint256, address) returns bytes32; function CVL_toId(Midnight.Market market) returns bytes32 { // Deterministically derive the market id. - bytes32 id = hash(market.loanToken, market.maturity, market.initialChainId, market.midnight); + bytes32 id = hash(market.loanToken, market.maturity, market.midnight); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index e68a3dc54..115d14d40 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -9,10 +9,10 @@ methods { function claimableSettlementFee(address token) external returns (uint256) envfree; } -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.initialChainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { diff --git a/src/Midnight.sol b/src/Midnight.sol index 382f1d3a0..d82ea76ef 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,9 +179,6 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 must be embedded in every market together with this Midnight -/// address, so a hard fork that changes block.chainid does not strand existing accounting. But as a result, after a -/// hard-fork there can be some market id clashes. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). @@ -190,10 +187,6 @@ contract Midnight is IMidnight { using UtilsLib for uint256; using UtilsLib for uint128; - /// IMMUTABLES /// - - uint256 public immutable INITIAL_CHAIN_ID; - /// STORAGE /// mapping(bytes32 id => mapping(address user => Position)) public position; @@ -213,8 +206,7 @@ contract Midnight is IMidnight { constructor() { roleSetter = msg.sender; - INITIAL_CHAIN_ID = block.chainid; - emit EventsLib.Constructor(msg.sender, INITIAL_CHAIN_ID); + emit EventsLib.Constructor(msg.sender); } /// MULTICALL /// @@ -778,10 +770,9 @@ contract Midnight is IMidnight { /// @dev Returns the market id and creates the market if it doesn't exist yet. function touchMarket(Market memory market) public returns (bytes32) { - require(market.initialChainId == INITIAL_CHAIN_ID, InvalidInitialChainId()); - require(market.midnight == address(this), InvalidMidnight()); bytes32 id = IdLib.toId(market); if (marketState[id].tickSpacing == 0) { + require(market.midnight == address(this), InvalidMidnight()); require(market.maturity <= block.timestamp + 100 * 365 days, MaturityTooFar()); require(market.collateralParams.length > 0, NoCollateralParams()); require(market.collateralParams.length <= MAX_COLLATERALS, TooManyCollateralParams()); diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 2c9a3636f..8abe2c9ae 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -3,7 +3,6 @@ pragma solidity >=0.5.0; struct Market { - uint256 initialChainId; address midnight; address loanToken; CollateralParams[] collateralParams; @@ -79,7 +78,6 @@ interface IMidnight { error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); error InvalidFeeIndex(); - error InvalidInitialChainId(); error InvalidLltv(); error InvalidMaxLif(); error InvalidMidnight(); @@ -121,9 +119,6 @@ interface IMidnight { error WrongSellCallbackReturnValue(); // forgefmt: disable-start - /// IMMUTABLES /// - function INITIAL_CHAIN_ID() external view returns (uint256); - /// STORAGE GETTERS /// function position(bytes32 id, address user) external view returns (uint128 credit, uint128 pendingFee, uint128 lastLossFactor, uint128 lastAccrual, uint128 debt, uint128 collateralBitmap); function marketState(bytes32 id) external view returns (uint128 totalUnits, uint128 lossFactor, uint128 withdrawable, uint128 continuousFeeCredit, uint16 settlementFeeCbp0, uint16 settlementFeeCbp1, uint16 settlementFeeCbp2, uint16 settlementFeeCbp3, uint16 settlementFeeCbp4, uint16 settlementFeeCbp5, uint16 settlementFeeCbp6, uint32 continuousFee, uint8 tickSpacing); diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 6fb835273..edcb54878 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -7,7 +7,7 @@ import {Market} from "../interfaces/IMidnight.sol"; /// @dev id_ is used to avoid naming conflicts in indexers. library EventsLib { // forgefmt: disable-start - event Constructor(address indexed roleSetter, uint256 initialChainId); + event Constructor(address indexed roleSetter); event SetRoleSetter(address indexed roleSetter); event SetFeeSetter(address indexed feeSetter); event SetTickSpacingSetter(address indexed tickSpacingSetter); diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index eb757aa14..c9b79ffe9 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -34,7 +34,7 @@ library IdLib { } /// @dev Stores the market in the code of the contract at its id-derived address. - /// @dev Uses zero as salt because initialChainId and midnight are part of the encoded market. + /// @dev Uses zero as salt because midnight is part of the encoded market. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { diff --git a/src/ratifiers/libraries/HashLib.sol b/src/ratifiers/libraries/HashLib.sol index 24d89a74d..99b34346b 100644 --- a/src/ratifiers/libraries/HashLib.sol +++ b/src/ratifiers/libraries/HashLib.sol @@ -7,9 +7,9 @@ import {Offer, Market, CollateralParams} from "../../interfaces/IMidnight.sol"; /// @dev keccak256("CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"). bytes32 constant COLLATERAL_PARAMS_TYPEHASH = 0xaf44a88eb50ebdbbebd980e5a23045c44f61ece5f80ab708a1bbe8718102e6af; /// @dev keccak256(bytes.concat(MARKET_TYPE, COLLATERAL_PARAMS_TYPE)). -bytes32 constant MARKET_TYPEHASH = 0x3fff861e0066633dc719c2511eb3c771b1517a7f8fad53b0bef7edd4a94d398d; +bytes32 constant MARKET_TYPEHASH = 0x9726017d91c6ee720433cc66705fc8e73c66a5b54318610634e0615c1050c039; /// @dev keccak256(bytes.concat(OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). -bytes32 constant OFFER_TYPEHASH = 0x2d9b2c48e9f7b87e10d277cc9eb2abc4a9cd2ec3451bd3990be472fe4ac78137; +bytes32 constant OFFER_TYPEHASH = 0x36b9157a5f6ca813c3e19022b1ae38bcc92474337a2905509648965b6bb25b92; library HashLib { error LeafIndexOutOfRange(); @@ -21,28 +21,28 @@ library HashLib { /// @dev Reverts if height is greater than 20. function offerTreeTypeHash(uint256 height) internal pure returns (bytes32) { if (height <= 10) { - if (height == 0) return 0xb7013fa5a999e5458627829111d031bbfa33e47889dca4e3514af04af509f09f; - if (height == 1) return 0xf815228253919597971ee6a6ca2adf2c3d8fc6296c7b86836ef214bd443daa58; - if (height == 2) return 0x893585f92cecb9f687cb75f92a57a6fc1754d8e5dad457ae1788158b26431712; - if (height == 3) return 0x86a3d14f6eb5735e5b94524e19d4c3d93ac15ac07ff41cc0a56ac928c6196b75; - if (height == 4) return 0xab8a93d114b72108b54341384805d18d28645e84d0c529c2c51953458047059f; - if (height == 5) return 0xb17a31924a5fc4d78bd61198e129de2fbdb46b962ed5c31ede6e96387491219f; - if (height == 6) return 0xbde56d2c07e3653be3d1f612a6f80ee60b75b327b5f2de0ebee98edea7b691db; - if (height == 7) return 0x6b2caaf1294590779ca6a569e9568e6980c547581cffff04f04a010e4bea0181; - if (height == 8) return 0x73b960155d0c9f339157d36610ccd76774a8dcd8a613f2127880e6d1f3590c5a; - if (height == 9) return 0xcc0480fc3e9396c4ecc52012adf81e481e0d1edecc3933d3b23e8150d26fd8e3; - return 0xada0eb530ea41beaf0af9972929d04e52040aaec090096b0598c39a34167bab6; + if (height == 0) return 0xf060e448a16febf6d574124babc538caa4e2b16b7d984b9a43138450fff6e487; + if (height == 1) return 0xdb7fcde9ede9a072d4b90099b7d70d3b078b8bd68eb7ebd656a7377363ac16e7; + if (height == 2) return 0x685d7c4569230738727c18fe3466e230e88e3910efa807662f1ace69be578cf8; + if (height == 3) return 0xc7447f9389248ec66fb8e2da0f33f2a75f14d028a260253ebd269062964a0ff0; + if (height == 4) return 0xdb2881d2334835a88923d1750f18f66230be7ad26cf6e365b8b66573a488f758; + if (height == 5) return 0xf998c5d024660a141601215a4c840a1e93cb734577da1f536f2e309eab9daa62; + if (height == 6) return 0x1a1546d3ee2cdcb87df2ba14cc402269536c5eb1a058559eea6b20b33886fec1; + if (height == 7) return 0xb970a1682bb961ff3038be80f6f0aa47a27ba955017ef22e1692da56a6fd583e; + if (height == 8) return 0xf44dac05caf7ffd668d73b72ba37867a3ed437ef625cfb517749609ab3e99683; + if (height == 9) return 0x8cd5c1d1c9277864113deaf1b2459d6688762fae010b3a6fd4ddd5befb85cf12; + return 0xebc93f3cf5dec43925a753dde4ca54c3615b34688066417d28200bd8c1ba7b5a; } else { - if (height == 11) return 0x81fa0e3280af655d1107469f0cd022c73f57a8b97b0ac19757162e4ccf21b6da; - if (height == 12) return 0x16f061da90a7f30a2af2f5ccc13be13a022de02a32aee285be67085861b8b913; - if (height == 13) return 0x7195f81e3204ac6040f29531f347d18dbe10552cd4618aba0793abb71cc6c19c; - if (height == 14) return 0x48659bfedd6ab7bbdaa4a0fb2491b9150191c54f84cdc6da5d2a4baea9f2722f; - if (height == 15) return 0x3f18ed4b2ad3720c79905889f1900cb770043edab868f1afa0d4cca5b7623773; - if (height == 16) return 0x3f939e01857b0725e963c412a35e5ee75cbc8c51d40cc5b33c11a4ef275b0086; - if (height == 17) return 0x990e2f81f815d7e02dc7cd5bc6aa2ea6631372ce9563a586dc60807107a7320c; - if (height == 18) return 0xf567d558ad0daa6e76e75e5bcba9d858ffcc43c1e48a98b07ce7665f41003158; - if (height == 19) return 0x957d376d258c67c3c69b7a67ac562821c7a2c5e9c189e19e8a041ba5e4a2e223; - if (height == 20) return 0x72e1d8247b3df82dfe2c47a385bba604682f719edf060edccd3a3e5c7e402849; + if (height == 11) return 0x0c284c1a76381ce275f1394cd25f795fd7f331e06dec54d7eed31d7092c1fbc8; + if (height == 12) return 0x2ac3bffae03e78dd4603a0e8702488b52bc2fa271b5e27cd58cd84f7bcc57e6a; + if (height == 13) return 0x42b99a1129d92a79d6757afb25f893eff4f31a3fb550be942b8abc31be8d4ace; + if (height == 14) return 0xdc8ec2491b7d8a693bd943aa90f544bcc3d03bdbf4ca11effc5ad64f5a54761e; + if (height == 15) return 0x6079f350a9f94248af09b389dde6562618f19dae07095660978985dc3767f683; + if (height == 16) return 0x8b2ea640243d598efc46c59303ff8853b43e9150ca72cb9bdd42bcb4a3e1e13c; + if (height == 17) return 0xbfadafebb38539f93a87280ecbc0dddf246a91a5bb6f279ab13e2dda19e1c639; + if (height == 18) return 0x9f1c3865762ee804df5d75220040b2164dfcd8ca336af4a0645a12fdbbbaf765; + if (height == 19) return 0x0d7255859b43f915c7eb1c6001be9a750c1bd2f9bc55528a97029143770dda95; + if (height == 20) return 0xf6ff0d681e5c27abedfcc9572b58e1c8314950e031448ee4954dc2573e3ff889; revert TreeTooHigh(); } } @@ -103,7 +103,6 @@ library HashLib { return keccak256( abi.encode( MARKET_TYPEHASH, - market.initialChainId, market.midnight, market.loanToken, collateralParamsHash, diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index 93f5ecc40..47c2ef4df 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -19,7 +19,6 @@ contract AuthorizationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/BaseTest.sol b/test/BaseTest.sol index 947ff016e..5a4eb215f 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -297,7 +297,6 @@ abstract contract BaseTest is Test { collateralParams[i].maxLif = maxLif(lltv, LIQUIDATION_CURSOR_LOW); } collateralParams = sortCollateralParams(collateralParams); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.collateralParams = collateralParams; market.maturity = bound(market.maturity, 0, vm.getBlockTimestamp() + 100 * 365 days); diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index cd6a48fcb..00dea99ab 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -23,7 +23,6 @@ contract ContinuousFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100 days; market.collateralParams diff --git a/test/EcrecoverRatifierIntegrationTest.sol b/test/EcrecoverRatifierIntegrationTest.sol index a4b6138b4..e592e4f50 100644 --- a/test/EcrecoverRatifierIntegrationTest.sol +++ b/test/EcrecoverRatifierIntegrationTest.sol @@ -25,7 +25,6 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/FrontendSignatureTest.sol b/test/FrontendSignatureTest.sol index 1643d47ae..1fe57a345 100644 --- a/test/FrontendSignatureTest.sol +++ b/test/FrontendSignatureTest.sol @@ -10,9 +10,9 @@ import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; // Paste from frontend output. address constant ACCOUNT = 0x6133Fd1B38C7D9ad120a6fC0D0f7b03d4F6E9658; -uint8 constant SIG_V = 28; -bytes32 constant SIG_R = 0xbdd5b0f61aafd17bcae94af84e87a9d7d80c3a54eb4e6ffb0b5efc78da812a01; -bytes32 constant SIG_S = 0x7b05c5581f10b0a79ac1bd89dfbf226e1c47921b9d68cfba47e5cd98d16a4c63; +uint8 constant SIG_V = 27; +bytes32 constant SIG_R = 0x802fd190b74cfb8998a0bbeeb95b1d26d761b777c00d4ca17d02251cfb2ff224; +bytes32 constant SIG_S = 0x252ae8e0d8e5c6b9b74986044c5c2c5c198d9fa9e155f4579cc769cbac1f74c1; address constant RATIFIER = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; @@ -25,7 +25,6 @@ contract FrontendSignatureTest is Test { function defaultOffer(uint8 number) internal pure returns (Offer memory offer) { CollateralParams[] memory collateralParams = new CollateralParams[](1); - offer.market.initialChainId = 1; offer.market.midnight = address(0); offer.market.loanToken = address(uint160(0x1111111111111111111111111111111111111111) * uint160(number)); offer.market.collateralParams = collateralParams; diff --git a/test/GateTest.sol b/test/GateTest.sol index 59d812b4f..872b0f703 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -43,7 +43,6 @@ contract GateTest is BaseTest { gate = new WhitelistGate(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams @@ -58,7 +57,6 @@ contract GateTest is BaseTest { market.collateralParams = sortCollateralParams(market.collateralParams); gatedMarket.loanToken = address(loanToken); - gatedMarket.initialChainId = midnight.INITIAL_CHAIN_ID(); gatedMarket.midnight = address(midnight); gatedMarket.maturity = vm.getBlockTimestamp() + 100; gatedMarket.collateralParams diff --git a/test/HashLibTest.sol b/test/HashLibTest.sol index 586e64bf3..72f430736 100644 --- a/test/HashLibTest.sol +++ b/test/HashLibTest.sol @@ -12,7 +12,7 @@ import {Market} from "../src/interfaces/IMidnight.sol"; bytes constant COLLATERAL_PARAMS_TYPE = "CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"; bytes constant MARKET_TYPE = - "Market(uint256 initialChainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; + "Market(address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; bytes constant OFFER_TYPE = "Offer(Market market,bool buy,address maker,uint256 start,uint256 expiry,uint256 tick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,address ratifier,bool reduceOnly,uint256 maxUnits,uint256 maxAssets,uint256 continuousFeeCap)"; @@ -37,7 +37,6 @@ contract HashLibTest is Test { bytes32 expectedHash = keccak256( abi.encode( MARKET_TYPEHASH, - market.initialChainId, market.midnight, market.loanToken, keccak256(abi.encodePacked(collateralParamsHashes)), diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index 2ad2d53dd..ba1946133 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -8,7 +8,6 @@ import {Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; // toMarket is tested in OtherFunctionsTest.sol, to test actual implementation (avoid introducing mocks). contract IdLibTest is Test { function baseMarket() internal pure returns (Market memory market) { - market.initialChainId = 1; market.midnight = address(1); market.loanToken = address(2); market.collateralParams = new CollateralParams[](1); @@ -20,13 +19,6 @@ contract IdLibTest is Test { market.liquidatorGate = address(6); } - function testToIdIsSensitiveToInitialChainId() public pure { - Market memory market1 = baseMarket(); - Market memory market2 = baseMarket(); - market2.initialChainId = 2; - assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - } - function testToIdIsSensitiveToMidnight() public pure { Market memory market1 = baseMarket(); Market memory market2 = baseMarket(); diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index e890443f7..4a739166a 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -45,7 +45,6 @@ contract LiquidationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index 079307508..022b17768 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -20,7 +20,6 @@ contract MaxAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index 2abde03fe..e03ba1a1f 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -46,7 +46,6 @@ contract MidnightBundlesTest is BaseTest { } market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index cb741c364..256989487 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -43,7 +43,6 @@ contract OtherFunctionsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams @@ -312,7 +311,6 @@ contract OtherFunctionsTest is BaseTest { bytes32 _id = midnight.touchMarket(_market); Market memory marketFromId = midnight.toMarket(_id); - assertEq(_market.initialChainId, marketFromId.initialChainId, "initialChainId"); assertEq(_market.midnight, marketFromId.midnight, "midnight"); assertEq(_market.loanToken, marketFromId.loanToken, "loanToken"); assertEq(_market.maturity, marketFromId.maturity, "maturity"); @@ -339,11 +337,8 @@ contract OtherFunctionsTest is BaseTest { _market = validMarket(_market); bytes32 idBefore = midnight.touchMarket(_market); - uint256 capturedChainId = midnight.INITIAL_CHAIN_ID(); - vm.chainId(newChainId); - assertEq(midnight.INITIAL_CHAIN_ID(), capturedChainId, "INITIAL_CHAIN_ID changed"); assertEq(toId(_market), idBefore, "toId changed"); Market memory roundTrip = midnight.toMarket(idBefore); assertEq(keccak256(abi.encode(roundTrip)), keccak256(abi.encode(_market)), "stored market lost"); @@ -359,15 +354,6 @@ contract OtherFunctionsTest is BaseTest { midnight.toMarket(_id); } - function testTouchMarketInvalidInitialChainId(Market memory _market) public { - vm.assume(_market.collateralParams.length > 0); - _market = validMarket(_market); - _market.initialChainId = midnight.INITIAL_CHAIN_ID() + 1; - - vm.expectRevert(IMidnight.InvalidInitialChainId.selector); - midnight.touchMarket(_market); - } - function testTouchMarketInvalidMidnight(Market memory _market) public { vm.assume(_market.collateralParams.length > 0); _market = validMarket(_market); @@ -401,7 +387,6 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); - marketWithRevertingOracle.initialChainId = midnight.INITIAL_CHAIN_ID(); marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -427,7 +412,6 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); - marketWithRevertingOracle.initialChainId = midnight.INITIAL_CHAIN_ID(); marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -457,7 +441,6 @@ contract OtherFunctionsTest is BaseTest { } collateralParams = sortCollateralParams(collateralParams); _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = collateralParams; @@ -468,7 +451,6 @@ contract OtherFunctionsTest is BaseTest { maturity = bound(maturity, vm.getBlockTimestamp() + 100 * 365 days + 1, type(uint256).max); Market memory longMarket; longMarket.loanToken = address(loanToken); - longMarket.initialChainId = midnight.INITIAL_CHAIN_ID(); longMarket.midnight = address(midnight); longMarket.maturity = maturity; longMarket.collateralParams = market.collateralParams; @@ -480,7 +462,6 @@ contract OtherFunctionsTest is BaseTest { function testZeroCollaterals() public { Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = new CollateralParams[](0); @@ -512,7 +493,6 @@ contract OtherFunctionsTest is BaseTest { function testCollateralsNotSorted() public { Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](2); @@ -531,7 +511,6 @@ contract OtherFunctionsTest is BaseTest { lltv = bound(lltv, WAD + 1, type(uint256).max); Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -548,7 +527,6 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.5e18; Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -686,7 +664,6 @@ contract OtherFunctionsTest is BaseTest { Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -702,7 +679,6 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -719,7 +695,6 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); - _market.initialChainId = midnight.INITIAL_CHAIN_ID(); _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 200; CollateralParams[] memory collateralParams = new CollateralParams[](1); diff --git a/test/SetterRatifierTest.sol b/test/SetterRatifierTest.sol index 8873d78c6..db602c7b6 100644 --- a/test/SetterRatifierTest.sol +++ b/test/SetterRatifierTest.sol @@ -21,7 +21,6 @@ contract SetterRatifierTest is BaseTest { function makeOffer(address maker) internal view returns (Offer memory offer) { Market memory market; market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams = new CollateralParams[](1); diff --git a/test/SettersTest.sol b/test/SettersTest.sol index f3a6a8e93..04db57a9b 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -35,7 +35,7 @@ contract SettersTest is BaseTest { function testConstructorEvent() public { vm.expectEmit(); - emit EventsLib.Constructor(address(this), block.chainid); + emit EventsLib.Constructor(address(this)); new Midnight(); } @@ -125,7 +125,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, @@ -301,7 +300,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, @@ -360,7 +358,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: address(0), maturity: vm.getBlockTimestamp() + 1 days, @@ -416,7 +413,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, @@ -445,7 +441,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, @@ -480,7 +475,6 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ - initialChainId: midnight.INITIAL_CHAIN_ID(), midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index 558c3050c..30cc5ccd6 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -38,7 +38,6 @@ contract SettlementFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); // to be able to come back in time enough market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 1 days; // TTM = 1 day (exactly at breakpoint) market.collateralParams diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index 653a712fe..087c8f3e9 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -22,7 +22,6 @@ contract TakeAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/TakeTest.sol b/test/TakeTest.sol index 83ae0bae7..dfe1e5bc7 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -35,7 +35,6 @@ contract TakeTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index cdab9fca4..66abebbc1 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -20,7 +20,6 @@ contract TickGatingTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); - market.initialChainId = midnight.INITIAL_CHAIN_ID(); market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/frontend/sign-root.ts b/test/frontend/sign-root.ts index fa71a1e20..e987beb87 100644 --- a/test/frontend/sign-root.ts +++ b/test/frontend/sign-root.ts @@ -32,7 +32,6 @@ function buildTypes(height: number) { { name: "oracle", type: "address" }, ], Market: [ - { name: "initialChainId", type: "uint256" }, { name: "midnight", type: "address" }, { name: "loanToken", type: "address" }, { name: "collateralParams", type: "CollateralParams[]" }, @@ -64,7 +63,6 @@ function buildTypes(height: number) { function defaultOffer(number: string) { return { market: { - initialChainId: "1", midnight: ZERO_ADDR, loanToken: "0x" + number.repeat(40), collateralParams: [{token: ZERO_ADDR, lltv: "0", maxLif: "0", oracle: ZERO_ADDR}], From d35a5cd6b9403ef3e609b682e46d23147c3c3e82 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:54:36 +0000 Subject: [PATCH 08/29] refactor: use market chain id without immutable Requested by: <@U02NB58F6R3> --- certora/specs/Role.spec | 4 +- certora/specs/SettlementFeeBoundaries.spec | 4 +- certora/specs/Solvency.spec | 4 +- certora/specs/WithdrawableMonotonicity.spec | 4 +- src/Midnight.sol | 1 + src/interfaces/IMidnight.sol | 2 + src/libraries/IdLib.sol | 2 +- src/ratifiers/libraries/HashLib.sol | 47 +++++++++++---------- test/AuthorizationTest.sol | 1 + test/BaseTest.sol | 1 + test/ContinuousFeeTest.sol | 1 + test/EcrecoverRatifierIntegrationTest.sol | 1 + test/FrontendSignatureTest.sol | 7 +-- test/GateTest.sol | 2 + test/HashLibTest.sol | 3 +- test/IdLibTest.sol | 8 ++++ test/LiquidationTest.sol | 1 + test/MaxAmountsTest.sol | 1 + test/MidnightBundlesTest.sol | 1 + test/OtherFunctionsTest.sol | 22 ++++++++++ test/SetterRatifierTest.sol | 1 + test/SettersTest.sol | 6 +++ test/SettlementFeeTest.sol | 1 + test/TakeAmountsTest.sol | 1 + test/TakeTest.sol | 1 + test/TickGatingTest.sol | 1 + test/frontend/sign-root.ts | 2 + 27 files changed, 94 insertions(+), 36 deletions(-) diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index 2cde8c70e..67d630858 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -29,10 +29,10 @@ methods { /// HELPERS /// -ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } definition WAD() returns uint256 = 10 ^ 18; diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index 9b0e4b1cb..b3ecdcc91 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -16,10 +16,10 @@ methods { function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } -ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index 3eb902747..5529c5b16 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -73,11 +73,11 @@ ghost mapping(bytes32 => address) loantoken; // Mapping from market id and collateral index to the corresponding collateral token. ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; -ghost hash(address, uint256, address) returns bytes32; +ghost hash(address, uint256, uint256, address) returns bytes32; function CVL_toId(Midnight.Market market) returns bytes32 { // Deterministically derive the market id. - bytes32 id = hash(market.loanToken, market.maturity, market.midnight); + bytes32 id = hash(market.loanToken, market.maturity, market.chainId, market.midnight); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 115d14d40..6593640d8 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -9,10 +9,10 @@ methods { function claimableSettlementFee(address token) external returns (uint256) envfree; } -ghost marketHash(address, address, uint256, uint256, address, address) returns bytes32; +ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); + return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { diff --git a/src/Midnight.sol b/src/Midnight.sol index d82ea76ef..4bc52a0b3 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -772,6 +772,7 @@ contract Midnight is IMidnight { function touchMarket(Market memory market) public returns (bytes32) { bytes32 id = IdLib.toId(market); if (marketState[id].tickSpacing == 0) { + require(market.chainId == block.chainid, InvalidChainId()); require(market.midnight == address(this), InvalidMidnight()); require(market.maturity <= block.timestamp + 100 * 365 days, MaturityTooFar()); require(market.collateralParams.length > 0, NoCollateralParams()); diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 8abe2c9ae..4f1dd5923 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -3,6 +3,7 @@ pragma solidity >=0.5.0; struct Market { + uint256 chainId; address midnight; address loanToken; CollateralParams[] collateralParams; @@ -78,6 +79,7 @@ interface IMidnight { error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); error InvalidFeeIndex(); + error InvalidChainId(); error InvalidLltv(); error InvalidMaxLif(); error InvalidMidnight(); diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index c9b79ffe9..f1f46555f 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -34,7 +34,7 @@ library IdLib { } /// @dev Stores the market in the code of the contract at its id-derived address. - /// @dev Uses zero as salt because midnight is part of the encoded market. + /// @dev Uses zero as salt because chainId and midnight are part of the encoded market. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { diff --git a/src/ratifiers/libraries/HashLib.sol b/src/ratifiers/libraries/HashLib.sol index 99b34346b..e480226c0 100644 --- a/src/ratifiers/libraries/HashLib.sol +++ b/src/ratifiers/libraries/HashLib.sol @@ -7,9 +7,9 @@ import {Offer, Market, CollateralParams} from "../../interfaces/IMidnight.sol"; /// @dev keccak256("CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"). bytes32 constant COLLATERAL_PARAMS_TYPEHASH = 0xaf44a88eb50ebdbbebd980e5a23045c44f61ece5f80ab708a1bbe8718102e6af; /// @dev keccak256(bytes.concat(MARKET_TYPE, COLLATERAL_PARAMS_TYPE)). -bytes32 constant MARKET_TYPEHASH = 0x9726017d91c6ee720433cc66705fc8e73c66a5b54318610634e0615c1050c039; +bytes32 constant MARKET_TYPEHASH = 0xc66c045aa2394a02e2976962976ec58c79108ae7fbb1ecc974c9724678b56264; /// @dev keccak256(bytes.concat(OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). -bytes32 constant OFFER_TYPEHASH = 0x36b9157a5f6ca813c3e19022b1ae38bcc92474337a2905509648965b6bb25b92; +bytes32 constant OFFER_TYPEHASH = 0x5e7c764a0f2411d16dd65139c973cbe0fe976b6d0736823e17aef319f652e7f8; library HashLib { error LeafIndexOutOfRange(); @@ -21,28 +21,28 @@ library HashLib { /// @dev Reverts if height is greater than 20. function offerTreeTypeHash(uint256 height) internal pure returns (bytes32) { if (height <= 10) { - if (height == 0) return 0xf060e448a16febf6d574124babc538caa4e2b16b7d984b9a43138450fff6e487; - if (height == 1) return 0xdb7fcde9ede9a072d4b90099b7d70d3b078b8bd68eb7ebd656a7377363ac16e7; - if (height == 2) return 0x685d7c4569230738727c18fe3466e230e88e3910efa807662f1ace69be578cf8; - if (height == 3) return 0xc7447f9389248ec66fb8e2da0f33f2a75f14d028a260253ebd269062964a0ff0; - if (height == 4) return 0xdb2881d2334835a88923d1750f18f66230be7ad26cf6e365b8b66573a488f758; - if (height == 5) return 0xf998c5d024660a141601215a4c840a1e93cb734577da1f536f2e309eab9daa62; - if (height == 6) return 0x1a1546d3ee2cdcb87df2ba14cc402269536c5eb1a058559eea6b20b33886fec1; - if (height == 7) return 0xb970a1682bb961ff3038be80f6f0aa47a27ba955017ef22e1692da56a6fd583e; - if (height == 8) return 0xf44dac05caf7ffd668d73b72ba37867a3ed437ef625cfb517749609ab3e99683; - if (height == 9) return 0x8cd5c1d1c9277864113deaf1b2459d6688762fae010b3a6fd4ddd5befb85cf12; - return 0xebc93f3cf5dec43925a753dde4ca54c3615b34688066417d28200bd8c1ba7b5a; + if (height == 0) return 0x04931ea05149e935551af887e04668a4235aa7fefe5a1307699fc36de5da3604; + if (height == 1) return 0x636eec88d23afdd7c5ecd3689cddd4a578f3640022a4f4b36532b1bb873870d9; + if (height == 2) return 0xb3b200d8a87156dd298add6829190724360e913ea2a544cf8770a83b1a85b68e; + if (height == 3) return 0x9959f9ee7df42c7c3a42ae48edb3544cb9672653eb4c9ad5190aa9ac194e13cc; + if (height == 4) return 0x859439b1a679d2b8d78d092af16a3e3abd30ccae9d29e12183c61dbd89069798; + if (height == 5) return 0x3b62f568d54c69f46baa3db29d9d16daa670a421eca545015e2c4d6ac5e2ab4b; + if (height == 6) return 0x06b027b3e518caa75c38007c1e4cffc92d314d528eb7ed63b28f6cbb250d6221; + if (height == 7) return 0x1948d59b3835088b65d9f7048f26cfcf508f9a750de32e7830b6f99c8915905a; + if (height == 8) return 0xfb12ec1a894c5cc36be99898bcc4c23d45713b8920d21ddf94ba0053fec835b4; + if (height == 9) return 0xee136fb578b4eea50b5929361278f0a09f97371d5c44ebb73b07785150f3c202; + return 0xdbd2c72ce0b8a438efba0500c50090d842804420fa9eb5819a265105ccf446d4; } else { - if (height == 11) return 0x0c284c1a76381ce275f1394cd25f795fd7f331e06dec54d7eed31d7092c1fbc8; - if (height == 12) return 0x2ac3bffae03e78dd4603a0e8702488b52bc2fa271b5e27cd58cd84f7bcc57e6a; - if (height == 13) return 0x42b99a1129d92a79d6757afb25f893eff4f31a3fb550be942b8abc31be8d4ace; - if (height == 14) return 0xdc8ec2491b7d8a693bd943aa90f544bcc3d03bdbf4ca11effc5ad64f5a54761e; - if (height == 15) return 0x6079f350a9f94248af09b389dde6562618f19dae07095660978985dc3767f683; - if (height == 16) return 0x8b2ea640243d598efc46c59303ff8853b43e9150ca72cb9bdd42bcb4a3e1e13c; - if (height == 17) return 0xbfadafebb38539f93a87280ecbc0dddf246a91a5bb6f279ab13e2dda19e1c639; - if (height == 18) return 0x9f1c3865762ee804df5d75220040b2164dfcd8ca336af4a0645a12fdbbbaf765; - if (height == 19) return 0x0d7255859b43f915c7eb1c6001be9a750c1bd2f9bc55528a97029143770dda95; - if (height == 20) return 0xf6ff0d681e5c27abedfcc9572b58e1c8314950e031448ee4954dc2573e3ff889; + if (height == 11) return 0x8201cbae421a17b9d8116ce28ecd13378b22d0257e9daca65ea354dc7b852e0a; + if (height == 12) return 0xb76e7eedf6cb4dfd2a913d292be9bbcdcdf6bc457e789306cc23a6917faca3d2; + if (height == 13) return 0x911f1aca18bfbc4e142c04b2020d972507d4b9d6b2fbef339bc48d33a438be9e; + if (height == 14) return 0xf8b8028014cfa85c41a0b5af6dd4d5e6c7236dab5886ee4f21e845ec4205443d; + if (height == 15) return 0xd517d3bc505b0209f539cf985e6252f0e57975ec5c8e3a93d97366f39c8cbbc5; + if (height == 16) return 0x35940ca810d4b02086a4d695dfaf04195c5a0fcb776e97c1349a8a0472b7bec8; + if (height == 17) return 0x44c0e3ddcf369c808911a5c78a3d7e87fa115a23cc40147b5fc40064c5560213; + if (height == 18) return 0x032fe7574172aadd1fb6c40e9deacfec41a48e2fc8de3574c67331c10e7b3a1f; + if (height == 19) return 0x3f80439b44bef469ff2390bdf0fd5469db472bb6748c6de9e5e2cd86d602c301; + if (height == 20) return 0x61068a1b8ffb6dd774e8a9634cb46a7a214dd84189e255072577c8541c543fd4; revert TreeTooHigh(); } } @@ -103,6 +103,7 @@ library HashLib { return keccak256( abi.encode( MARKET_TYPEHASH, + market.chainId, market.midnight, market.loanToken, collateralParamsHash, diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index 47c2ef4df..a1eb9d21e 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -19,6 +19,7 @@ contract AuthorizationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/BaseTest.sol b/test/BaseTest.sol index 5a4eb215f..3a09501d3 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -297,6 +297,7 @@ abstract contract BaseTest is Test { collateralParams[i].maxLif = maxLif(lltv, LIQUIDATION_CURSOR_LOW); } collateralParams = sortCollateralParams(collateralParams); + market.chainId = block.chainid; market.midnight = address(midnight); market.collateralParams = collateralParams; market.maturity = bound(market.maturity, 0, vm.getBlockTimestamp() + 100 * 365 days); diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index 00dea99ab..4ce87b710 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -23,6 +23,7 @@ contract ContinuousFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100 days; market.collateralParams diff --git a/test/EcrecoverRatifierIntegrationTest.sol b/test/EcrecoverRatifierIntegrationTest.sol index e592e4f50..4aa5bf153 100644 --- a/test/EcrecoverRatifierIntegrationTest.sol +++ b/test/EcrecoverRatifierIntegrationTest.sol @@ -25,6 +25,7 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/FrontendSignatureTest.sol b/test/FrontendSignatureTest.sol index 1fe57a345..dcbfed083 100644 --- a/test/FrontendSignatureTest.sol +++ b/test/FrontendSignatureTest.sol @@ -10,9 +10,9 @@ import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; // Paste from frontend output. address constant ACCOUNT = 0x6133Fd1B38C7D9ad120a6fC0D0f7b03d4F6E9658; -uint8 constant SIG_V = 27; -bytes32 constant SIG_R = 0x802fd190b74cfb8998a0bbeeb95b1d26d761b777c00d4ca17d02251cfb2ff224; -bytes32 constant SIG_S = 0x252ae8e0d8e5c6b9b74986044c5c2c5c198d9fa9e155f4579cc769cbac1f74c1; +uint8 constant SIG_V = 28; +bytes32 constant SIG_R = 0x70edf1a8f911f5f66575f3f9a2f8a4ac5b7ce80da66fe6a9f1d3a1f527fb7d04; +bytes32 constant SIG_S = 0x32e9f29d8f20c1116b3c1484b203d2e5e6c295161de512569ca5502fd110cb36; address constant RATIFIER = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; @@ -25,6 +25,7 @@ contract FrontendSignatureTest is Test { function defaultOffer(uint8 number) internal pure returns (Offer memory offer) { CollateralParams[] memory collateralParams = new CollateralParams[](1); + offer.market.chainId = 1; offer.market.midnight = address(0); offer.market.loanToken = address(uint160(0x1111111111111111111111111111111111111111) * uint160(number)); offer.market.collateralParams = collateralParams; diff --git a/test/GateTest.sol b/test/GateTest.sol index 872b0f703..a4d9936fd 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -43,6 +43,7 @@ contract GateTest is BaseTest { gate = new WhitelistGate(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams @@ -57,6 +58,7 @@ contract GateTest is BaseTest { market.collateralParams = sortCollateralParams(market.collateralParams); gatedMarket.loanToken = address(loanToken); + gatedMarket.chainId = block.chainid; gatedMarket.midnight = address(midnight); gatedMarket.maturity = vm.getBlockTimestamp() + 100; gatedMarket.collateralParams diff --git a/test/HashLibTest.sol b/test/HashLibTest.sol index 72f430736..1369a6f34 100644 --- a/test/HashLibTest.sol +++ b/test/HashLibTest.sol @@ -12,7 +12,7 @@ import {Market} from "../src/interfaces/IMidnight.sol"; bytes constant COLLATERAL_PARAMS_TYPE = "CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"; bytes constant MARKET_TYPE = - "Market(address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; + "Market(uint256 chainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; bytes constant OFFER_TYPE = "Offer(Market market,bool buy,address maker,uint256 start,uint256 expiry,uint256 tick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,address ratifier,bool reduceOnly,uint256 maxUnits,uint256 maxAssets,uint256 continuousFeeCap)"; @@ -37,6 +37,7 @@ contract HashLibTest is Test { bytes32 expectedHash = keccak256( abi.encode( MARKET_TYPEHASH, + market.chainId, market.midnight, market.loanToken, keccak256(abi.encodePacked(collateralParamsHashes)), diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index ba1946133..2c6d587b4 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -8,6 +8,7 @@ import {Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; // toMarket is tested in OtherFunctionsTest.sol, to test actual implementation (avoid introducing mocks). contract IdLibTest is Test { function baseMarket() internal pure returns (Market memory market) { + market.chainId = 1; market.midnight = address(1); market.loanToken = address(2); market.collateralParams = new CollateralParams[](1); @@ -19,6 +20,13 @@ contract IdLibTest is Test { market.liquidatorGate = address(6); } + function testToIdIsSensitiveToChainId() public pure { + Market memory market1 = baseMarket(); + Market memory market2 = baseMarket(); + market2.chainId = 2; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + } + function testToIdIsSensitiveToMidnight() public pure { Market memory market1 = baseMarket(); Market memory market2 = baseMarket(); diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 4a739166a..5fb0116f8 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -45,6 +45,7 @@ contract LiquidationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index 022b17768..9b5592f60 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -20,6 +20,7 @@ contract MaxAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index e03ba1a1f..551c108b2 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -46,6 +46,7 @@ contract MidnightBundlesTest is BaseTest { } market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 256989487..210b33484 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -43,6 +43,7 @@ contract OtherFunctionsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams @@ -311,6 +312,7 @@ contract OtherFunctionsTest is BaseTest { bytes32 _id = midnight.touchMarket(_market); Market memory marketFromId = midnight.toMarket(_id); + assertEq(_market.chainId, marketFromId.chainId, "chainId"); assertEq(_market.midnight, marketFromId.midnight, "midnight"); assertEq(_market.loanToken, marketFromId.loanToken, "loanToken"); assertEq(_market.maturity, marketFromId.maturity, "maturity"); @@ -354,6 +356,15 @@ contract OtherFunctionsTest is BaseTest { midnight.toMarket(_id); } + function testTouchMarketInvalidChainId(Market memory _market) public { + vm.assume(_market.collateralParams.length > 0); + _market = validMarket(_market); + _market.chainId = block.chainid + 1; + + vm.expectRevert(IMidnight.InvalidChainId.selector); + midnight.touchMarket(_market); + } + function testTouchMarketInvalidMidnight(Market memory _market) public { vm.assume(_market.collateralParams.length > 0); _market = validMarket(_market); @@ -387,6 +398,7 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.chainId = block.chainid; marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -412,6 +424,7 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.chainId = block.chainid; marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -441,6 +454,7 @@ contract OtherFunctionsTest is BaseTest { } collateralParams = sortCollateralParams(collateralParams); _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = collateralParams; @@ -451,6 +465,7 @@ contract OtherFunctionsTest is BaseTest { maturity = bound(maturity, vm.getBlockTimestamp() + 100 * 365 days + 1, type(uint256).max); Market memory longMarket; longMarket.loanToken = address(loanToken); + longMarket.chainId = block.chainid; longMarket.midnight = address(midnight); longMarket.maturity = maturity; longMarket.collateralParams = market.collateralParams; @@ -462,6 +477,7 @@ contract OtherFunctionsTest is BaseTest { function testZeroCollaterals() public { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = new CollateralParams[](0); @@ -493,6 +509,7 @@ contract OtherFunctionsTest is BaseTest { function testCollateralsNotSorted() public { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](2); @@ -511,6 +528,7 @@ contract OtherFunctionsTest is BaseTest { lltv = bound(lltv, WAD + 1, type(uint256).max); Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -527,6 +545,7 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.5e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -664,6 +683,7 @@ contract OtherFunctionsTest is BaseTest { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -679,6 +699,7 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); @@ -695,6 +716,7 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 200; CollateralParams[] memory collateralParams = new CollateralParams[](1); diff --git a/test/SetterRatifierTest.sol b/test/SetterRatifierTest.sol index db602c7b6..092538f46 100644 --- a/test/SetterRatifierTest.sol +++ b/test/SetterRatifierTest.sol @@ -21,6 +21,7 @@ contract SetterRatifierTest is BaseTest { function makeOffer(address maker) internal view returns (Offer memory offer) { Market memory market; market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams = new CollateralParams[](1); diff --git a/test/SettersTest.sol b/test/SettersTest.sol index 04db57a9b..c840d2139 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -125,6 +125,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, @@ -300,6 +301,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, @@ -358,6 +360,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: address(0), maturity: vm.getBlockTimestamp() + 1 days, @@ -413,6 +416,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, @@ -441,6 +445,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, @@ -475,6 +480,7 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index 30cc5ccd6..4c8cd0fe3 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -38,6 +38,7 @@ contract SettlementFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); // to be able to come back in time enough market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 1 days; // TTM = 1 day (exactly at breakpoint) market.collateralParams diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index 087c8f3e9..853fedfea 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -22,6 +22,7 @@ contract TakeAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/TakeTest.sol b/test/TakeTest.sol index dfe1e5bc7..3afc6c134 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -35,6 +35,7 @@ contract TakeTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index 66abebbc1..3db63593c 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -20,6 +20,7 @@ contract TickGatingTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams diff --git a/test/frontend/sign-root.ts b/test/frontend/sign-root.ts index e987beb87..817845cf8 100644 --- a/test/frontend/sign-root.ts +++ b/test/frontend/sign-root.ts @@ -32,6 +32,7 @@ function buildTypes(height: number) { { name: "oracle", type: "address" }, ], Market: [ + { name: "chainId", type: "uint256" }, { name: "midnight", type: "address" }, { name: "loanToken", type: "address" }, { name: "collateralParams", type: "CollateralParams[]" }, @@ -63,6 +64,7 @@ function buildTypes(height: number) { function defaultOffer(number: string) { return { market: { + chainId: "1", midnight: ZERO_ADDR, loanToken: "0x" + number.repeat(40), collateralParams: [{token: ZERO_ADDR, lltv: "0", maxLif: "0", oracle: ZERO_ADDR}], From 97c69c0ff76db6ae6cf30aae86c4edaf62e76933 Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:51:31 +0200 Subject: [PATCH 09/29] Apply suggestion from @MathisGD Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/libraries/IdLib.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index f1f46555f..41eebdd4a 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -34,7 +34,6 @@ library IdLib { } /// @dev Stores the market in the code of the contract at its id-derived address. - /// @dev Uses zero as salt because chainId and midnight are part of the encoded market. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { From de7407428c64b298448faeb79f5ad71d53a54bec Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:52:08 +0000 Subject: [PATCH 10/29] refactor: expose toId through certora helper Requested by: <@U02NB58F6R3> --- certora/confs/BalanceEffects.conf | 1 + certora/confs/WithdrawableMonotonicity.conf | 1 + certora/helpers/Utils.sol | 5 +++++ certora/specs/BalanceEffects.spec | 25 +++++++++++---------- certora/specs/Role.spec | 10 ++------- certora/specs/SettlementFeeBoundaries.spec | 10 ++------- certora/specs/WithdrawableMonotonicity.spec | 18 ++++++--------- src/libraries/IdLib.sol | 1 + 8 files changed, 32 insertions(+), 39 deletions(-) diff --git a/certora/confs/BalanceEffects.conf b/certora/confs/BalanceEffects.conf index c61ec3903..479bc7233 100644 --- a/certora/confs/BalanceEffects.conf +++ b/certora/confs/BalanceEffects.conf @@ -1,5 +1,6 @@ { "files": [ + "certora/helpers/Utils.sol", "src/Midnight.sol" ], "verify": "Midnight:certora/specs/BalanceEffects.spec", diff --git a/certora/confs/WithdrawableMonotonicity.conf b/certora/confs/WithdrawableMonotonicity.conf index 08f85f116..259492c7c 100644 --- a/certora/confs/WithdrawableMonotonicity.conf +++ b/certora/confs/WithdrawableMonotonicity.conf @@ -1,5 +1,6 @@ { "files": [ + "certora/helpers/Utils.sol", "src/Midnight.sol" ], "verify": "Midnight:certora/specs/WithdrawableMonotonicity.spec", diff --git a/certora/helpers/Utils.sol b/certora/helpers/Utils.sol index af7b56e96..709a5d57f 100644 --- a/certora/helpers/Utils.sol +++ b/certora/helpers/Utils.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Offer, Market} from "../../src/interfaces/IMidnight.sol"; import {UtilsLib} from "../../src/libraries/UtilsLib.sol"; +import {IdLib} from "../../src/libraries/IdLib.sol"; import { CALLBACK_SUCCESS, LIQUIDATION_CURSOR_LOW, @@ -14,6 +15,10 @@ import { } from "../../src/libraries/ConstantsLib.sol"; contract Utils { + function toId(Market memory market) external pure returns (bytes32) { + return IdLib.toId(market); + } + function hashMarket(Market memory market) external pure returns (bytes32) { return keccak256(abi.encode(market)); } diff --git a/certora/specs/BalanceEffects.spec b/certora/specs/BalanceEffects.spec index fa383bd7a..0d70660ed 100644 --- a/certora/specs/BalanceEffects.spec +++ b/certora/specs/BalanceEffects.spec @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - 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; @@ -36,7 +37,7 @@ methods { /// sets it to the post-update value, only changes credit of user at the market id, /// and accrues fee to continuousFeeCredit. rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 anyId, address anyUser) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 creditBefore = credit(id, user); uint128 updatedUserCredit; @@ -63,7 +64,7 @@ rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 /// withdraw decreases onBehalf's post-update credit by exactly units /// and only changes credit of onBehalf at the market id. rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address receiver, bytes32 anyId, address anyUser) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint128 updatedUserCredit; uint128 userFee; @@ -86,7 +87,7 @@ rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBeh /// take changes maker's and taker's net credit-debt by +/- units relative to their post-update values /// and only changes credit of maker and taker and debt of maker and taker at the market id. rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData, bytes32 anyId, address anyUser) { - bytes32 id = summaryToId(offer.market); + bytes32 id = Utils.toId(offer.market); uint128 makerCreditBefore; makerCreditBefore, _, _ = updatePositionView(e, offer.market, id, offer.maker); @@ -114,7 +115,7 @@ rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, /// Buyer's credit is non-decreasing relative to its post-update value and can increase by at most take units. /// Buyer's debt is non-increasing and can decrease by at most take units. rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = summaryToId(offer.market); + bytes32 id = Utils.toId(offer.market); address buyer = offer.buy ? offer.maker : taker; uint256 buyerDebtBefore = debt(id, buyer); @@ -134,7 +135,7 @@ rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 u /// Seller's debt is non-decreasing, and can increase by at most take units. /// Seller's credit is non-increasing relative to its post-update value and can decrease by at most take units. rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = summaryToId(offer.market); + bytes32 id = Utils.toId(offer.market); address seller = offer.buy ? taker : offer.maker; uint256 sellerDebtBefore = debt(id, seller); @@ -154,7 +155,7 @@ rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 /// Repay decreases onBehalf's debt by exactly units and only changes position[id][onBehalf].debt rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data, bytes32 anyId, address anyUser) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 debtBefore = debt(id, onBehalf); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -172,7 +173,7 @@ rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf /// Liquidate decreases the borrower's debt by at least repaidUnits, /// and only changes position[id][borrower].debt. 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 = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 debtBefore = debt(id, borrower); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -211,7 +212,7 @@ filtered { /// supplyCollateral increases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -227,7 +228,7 @@ rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIn /// withdrawCollateral decreases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, address receiver, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -243,7 +244,7 @@ rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 /// liquidate decreases the borrower's collateral at collateralIndex by exactly seizedResult, /// and only changes position[id][borrower].collateral[collateralIndex]. rule liquidateCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bytes32 anyId, address anyUser, uint256 anyIndex, bool postMaturityMode) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, borrower, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index 67d630858..29e255477 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -3,10 +3,9 @@ using Utils as Utils; methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - function roleSetter() external returns (address) envfree; function feeSetter() external returns (address) envfree; function feeClaimer() external returns (address) envfree; @@ -29,11 +28,6 @@ methods { /// HELPERS /// -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; - -function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); -} definition WAD() returns uint256 = 10 ^ 18; @@ -278,7 +272,7 @@ rule feeClaimerCanClaimSettlementFee(env e, address token, uint256 amount, addre } rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver, address user) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); address feeClaimerBefore = feeClaimer(); bool marketIsCreated = marketIsCreated(id); uint256 withdrawableBefore = withdrawable(id); diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index b3ecdcc91..28eff31b1 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -3,10 +3,9 @@ using Utils as Utils; methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - function settlementFee(bytes32 id, uint256 timeToMaturity) external returns (uint256) envfree; function feeSetter() external returns (address) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; @@ -16,11 +15,6 @@ methods { function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; - -function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); -} /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. definition breakpointTime(uint256 index) returns uint256 = index == 0 ? 0 : index == 1 ? 86400 : index == 2 ? 7 * 86400 : index == 3 ? 30 * 86400 : index == 4 ? 90 * 86400 : index == 5 ? 180 * 86400 : index == 6 ? 360 * 86400 : 0; @@ -73,7 +67,7 @@ invariant marketSettlementFeePerIndexBound(bytes32 id, uint256 index) /// When a market is created, its settlement fees are set to the default settlement fees of its loan token. rule newMarketSettlementFeesMatchDefault(env e, Midnight.Market market, uint256 index) { require index <= 6, "index out of bounds"; - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); require tickSpacing(id) == 0, "market not yet created"; uint256 expectedSettlementFee = defaultSettlementFee(market.loanToken, index); diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 6593640d8..447eb71e2 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -1,22 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - function withdrawable(bytes32 id) external returns (uint128) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; } -ghost marketHash(uint256, address, address, uint256, uint256, address, address) returns bytes32; - -function summaryToId(Midnight.Market market) returns bytes32 { - return marketHash(market.chainId, market.midnight, market.loanToken, market.maturity, market.rcfThreshold, market.enterGate, market.liquidatorGate); -} rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); repay(e, market, units, onBehalf, callback, data); uint256 withdrawableAfter = withdrawable(id); @@ -24,7 +20,7 @@ rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, ad } rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); uint256 seizedResult; uint256 repaidResult; @@ -34,7 +30,7 @@ rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 colla } rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 unitsInput, address onBehalf, address receiver) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); withdraw(e, market, unitsInput, onBehalf, receiver); uint256 withdrawableAfter = withdrawable(id); @@ -42,7 +38,7 @@ rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 } rule claimContinuousFeeDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 amount, address receiver) { - bytes32 id = summaryToId(market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); claimContinuousFee(e, market, amount, receiver); uint256 withdrawableAfter = withdrawable(id); diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index 41eebdd4a..f1f46555f 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -34,6 +34,7 @@ library IdLib { } /// @dev Stores the market in the code of the contract at its id-derived address. + /// @dev Uses zero as salt because chainId and midnight are part of the encoded market. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { From 7da1c0c78b0d84c29add90768b65873aa1a53599 Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:54:04 +0200 Subject: [PATCH 11/29] Apply suggestion from @MathisGD Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/libraries/IdLib.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index f1f46555f..41eebdd4a 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -34,7 +34,6 @@ library IdLib { } /// @dev Stores the market in the code of the contract at its id-derived address. - /// @dev Uses zero as salt because chainId and midnight are part of the encoded market. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { From b05377d5ca686994275ad3bf3b1c8294f232dba0 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:54:35 +0000 Subject: [PATCH 12/29] style: fix certora role formatting Requested by: <@U02NB58F6R3> --- certora/specs/Role.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index 29e255477..bbc70600e 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -28,7 +28,6 @@ methods { /// HELPERS /// - definition WAD() returns uint256 = 10 ^ 18; definition CBP() returns uint256 = 10 ^ 12; From cbb686b44dc64298e3949b311e5c485e3e3c2597 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:57:08 +0000 Subject: [PATCH 13/29] style: fix certora formatter spacing Requested by: <@U02NB58F6R3> --- certora/specs/SettlementFeeBoundaries.spec | 1 - certora/specs/WithdrawableMonotonicity.spec | 1 - 2 files changed, 2 deletions(-) diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index 28eff31b1..d13808672 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -15,7 +15,6 @@ methods { function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; } - /// Breakpoint time in seconds for index 0..6, mirroring the settlementFee intervals in Midnight.sol. definition breakpointTime(uint256 index) returns uint256 = index == 0 ? 0 : index == 1 ? 86400 : index == 2 ? 7 * 86400 : index == 3 ? 30 * 86400 : index == 4 ? 90 * 86400 : index == 5 ? 180 * 86400 : index == 6 ? 360 * 86400 : 0; diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 447eb71e2..00c23f9bb 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -10,7 +10,6 @@ methods { function claimableSettlementFee(address token) external returns (uint256) envfree; } - rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); From 5290899c2700c12709504dc78a24600ad4e4e93f Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:52:41 +0200 Subject: [PATCH 14/29] Apply suggestion from @MathisGD Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Midnight.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Midnight.sol b/src/Midnight.sol index 4bc52a0b3..ff249b167 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,6 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Market ids can clash after a hard-fork since they are identified with the chain id at their creation. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). From ae6df75ae03d4ca6a8190b5b3172ebf3f7d2d6e6 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:21:47 +0200 Subject: [PATCH 15/29] fix(certora): constrain global market domain fields --- certora/specs/Healthiness.spec | 6 ++++-- certora/specs/NoDivisionByZero.spec | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/certora/specs/Healthiness.spec b/certora/specs/Healthiness.spec index 1c602ce65..93c2b5245 100644 --- a/certora/specs/Healthiness.spec +++ b/certora/specs/Healthiness.spec @@ -102,6 +102,8 @@ persistent ghost bool healthyOrLockedBeforeCallbacks; // global variable to track which market and borrower we're testing. persistent ghost address globalMarketLoanToken; +persistent ghost uint256 globalMarketChainId; + persistent ghost uint256 globalMarketCollateralLength; persistent ghost mapping(uint256 => address) globalMarketCollateralOracle; @@ -129,7 +131,7 @@ persistent ghost address globalBorrower; definition collateralMatches(Midnight.Market market, uint256 index) returns bool = (index < globalMarketCollateralLength => market.collateralParams[index].oracle == globalMarketCollateralOracle[index] && market.collateralParams[index].token == globalMarketCollateralToken[index] && market.collateralParams[index].lltv == globalMarketCollateralLLTV[index] && market.collateralParams[index].maxLif == globalMarketCollateralMaxLif[index]); function equalsGlobalMarket(Midnight.Market market) returns (bool) { - return market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; + return market.chainId == globalMarketChainId && market.midnight == currentContract && market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; } function getGlobalMarket() returns (Midnight.Market) { @@ -140,7 +142,7 @@ function getGlobalMarket() returns (Midnight.Market) { function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && market.midnight == currentContract) { + if (equalsGlobalMarket(market)) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; diff --git a/certora/specs/NoDivisionByZero.spec b/certora/specs/NoDivisionByZero.spec index e6d972fc5..f0b2efeaa 100644 --- a/certora/specs/NoDivisionByZero.spec +++ b/certora/specs/NoDivisionByZero.spec @@ -35,6 +35,8 @@ methods { persistent ghost address globalMarketLoanToken; +persistent ghost uint256 globalMarketChainId; + persistent ghost uint256 globalMarketCollateralLength; persistent ghost mapping(uint256 => address) globalMarketCollateralOracle; @@ -71,12 +73,12 @@ definition WAD() returns uint256 = 10 ^ 18; definition collateralMatches(Midnight.Market market, uint256 index) returns bool = (index < globalMarketCollateralLength => market.collateralParams[index].oracle == globalMarketCollateralOracle[index] && market.collateralParams[index].token == globalMarketCollateralToken[index] && market.collateralParams[index].lltv == globalMarketCollateralLLTV[index] && market.collateralParams[index].maxLif == globalMarketCollateralMaxLif[index]); function equalsGlobalMarket(Midnight.Market market) returns (bool) { - return market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; + return market.chainId == globalMarketChainId && market.midnight == currentContract && market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; } function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && market.midnight == currentContract) { + if (equalsGlobalMarket(market)) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; From d4e247f2de70ab4834c559aacd8310af9c929561 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:44:55 +0200 Subject: [PATCH 16/29] fix(certora): key solvency ids by full market --- certora/confs/Solvency.conf | 1 + certora/specs/Solvency.spec | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/certora/confs/Solvency.conf b/certora/confs/Solvency.conf index 886295bd1..a96ab4fe4 100644 --- a/certora/confs/Solvency.conf +++ b/certora/confs/Solvency.conf @@ -1,6 +1,7 @@ { "files": [ "src/Midnight.sol", + "certora/helpers/Utils.sol", "certora/helpers/FlashLiquidateCallback.sol" ], "parametric_contracts": [ diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index 5529c5b16..dd6b791b6 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function claimableSettlementFee(address token) external returns (uint256) envfree; @@ -73,11 +76,9 @@ ghost mapping(bytes32 => address) loantoken; // Mapping from market id and collateral index to the corresponding collateral token. ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; -ghost hash(address, uint256, uint256, address) returns bytes32; - function CVL_toId(Midnight.Market market) returns bytes32 { - // Deterministically derive the market id. - bytes32 id = hash(market.loanToken, market.maturity, market.chainId, market.midnight); + // Deterministically derive the market id from the full market. + bytes32 id = Utils.hashMarket(market); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. From 7e84864230ee23441dc43207b10f7fd6984e36a5 Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:02:33 +0200 Subject: [PATCH 17/29] Update IdLib.sol Co-authored-by: Adrien Husson Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/libraries/IdLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index 41eebdd4a..972b1457e 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -33,7 +33,7 @@ library IdLib { ); } - /// @dev Stores the market in the code of the contract at its id-derived address. + /// @dev Stores the market in the code of the contract at an address equal to the last 20 bytes of its id. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { From db4b174a42b7d29806b3fcc88681d054153a396b Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 00:13:59 +0200 Subject: [PATCH 18/29] fix(midnight): address review comments --- certora/confs/BalanceEffects.conf | 3 +++ certora/confs/WithdrawableMonotonicity.conf | 3 +++ certora/specs/Solvency.spec | 6 ++++-- src/Midnight.sol | 8 +++----- test/ContinuousFeeTest.sol | 19 ++++++++++++++----- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/certora/confs/BalanceEffects.conf b/certora/confs/BalanceEffects.conf index 479bc7233..1cf8922ab 100644 --- a/certora/confs/BalanceEffects.conf +++ b/certora/confs/BalanceEffects.conf @@ -3,6 +3,9 @@ "certora/helpers/Utils.sol", "src/Midnight.sol" ], + "parametric_contracts": [ + "Midnight" + ], "verify": "Midnight:certora/specs/BalanceEffects.spec", "solc": "solc-0.8.34", "solc_via_ir": true, diff --git a/certora/confs/WithdrawableMonotonicity.conf b/certora/confs/WithdrawableMonotonicity.conf index 259492c7c..92d89d1ba 100644 --- a/certora/confs/WithdrawableMonotonicity.conf +++ b/certora/confs/WithdrawableMonotonicity.conf @@ -3,6 +3,9 @@ "certora/helpers/Utils.sol", "src/Midnight.sol" ], + "parametric_contracts": [ + "Midnight" + ], "verify": "Midnight:certora/specs/WithdrawableMonotonicity.spec", "solc": "solc-0.8.34", "solc_via_ir": true, diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index dd6b791b6..4b34e22e9 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -76,9 +76,11 @@ ghost mapping(bytes32 => address) loantoken; // Mapping from market id and collateral index to the corresponding collateral token. ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; +ghost hash(bytes32) returns bytes32; + function CVL_toId(Midnight.Market market) returns bytes32 { - // Deterministically derive the market id from the full market. - bytes32 id = Utils.hashMarket(market); + // Deterministically derive an arbitrary market id from the full market. + bytes32 id = hash(Utils.hashMarket(market)); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. diff --git a/src/Midnight.sol b/src/Midnight.sol index ff249b167..df2b4d1df 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,7 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Market ids can clash after a hard-fork since they are identified with the chain id at their creation. +/// @dev Market ids are scoped by market.chainId; identical markets on chains sharing a chain id may share ids. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). @@ -322,10 +322,9 @@ contract Midnight is IMidnight { } function claimContinuousFee(Market memory market, uint256 amount, address receiver) external { - bytes32 id = IdLib.toId(market); + bytes32 id = touchMarket(market); MarketState storage _marketState = marketState[id]; require(msg.sender == feeClaimer, OnlyFeeClaimer()); - require(_marketState.tickSpacing > 0, MarketNotCreated()); _marketState.continuousFeeCredit -= UtilsLib.toUint128(amount); _marketState.totalUnits -= UtilsLib.toUint128(amount); @@ -841,8 +840,7 @@ contract Midnight is IMidnight { /// @dev Slashes the position and accrues the continuous fee. /// @dev Returns the new credit, new pending fee, and accrued fee after having updated the position. function updatePosition(Market memory market, address user) external returns (uint128, uint128, uint128) { - bytes32 id = IdLib.toId(market); - require(marketState[id].tickSpacing > 0, MarketNotCreated()); + bytes32 id = touchMarket(market); return _updatePosition(market, id, user); } diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index 4ce87b710..090716dc1 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -560,15 +560,24 @@ contract ContinuousFeeTest is BaseTest { assertEq(midnight.continuousFeeCredit(id), expectedContinuousFeeCredit, "continuousFeeCredit"); } - function testUpdatePositionRevertsIfMarketNotCreated() public { - vm.expectRevert(IMidnight.MarketNotCreated.selector); - midnight.updatePosition(market, borrower); + function testUpdatePositionTouchesMarketIfNotCreated() public { + assertEq(midnight.tickSpacing(id), 0, "market not created before"); + + (uint128 credit, uint128 pendingFee, uint128 accruedFee) = midnight.updatePosition(market, borrower); + + assertEq(credit, 0, "credit"); + assertEq(pendingFee, 0, "pendingFee"); + assertEq(accruedFee, 0, "accruedFee"); + assertGt(midnight.tickSpacing(id), 0, "market created after"); } - function testClaimContinuousFeeRevertsIfMarketNotCreated() public { + function testClaimContinuousFeeTouchesMarketIfNotCreated() public { + assertEq(midnight.tickSpacing(id), 0, "market not created before"); + vm.prank(feeClaimer); - vm.expectRevert(IMidnight.MarketNotCreated.selector); midnight.claimContinuousFee(market, 0, feeClaimer); + + assertGt(midnight.tickSpacing(id), 0, "market created after"); } function testLastAccrualZeroForFreshPosition() public { From f6690902a68408bd3cf8073b74b0a6d8b57c5c07 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:02:08 +0200 Subject: [PATCH 19/29] fix(certora): allow fee claim and position market creation --- certora/specs/CreatedMarkets.spec | 14 +++++++++++++- certora/specs/LossFactor.spec | 1 + certora/specs/Role.spec | 6 ++++-- certora/specs/SettlementFeeBoundaries.spec | 6 ++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/certora/specs/CreatedMarkets.spec b/certora/specs/CreatedMarkets.spec index 8a5618e83..59583afa7 100644 --- a/certora/specs/CreatedMarkets.spec +++ b/certora/specs/CreatedMarkets.spec @@ -130,7 +130,17 @@ rule marketIsCreatedAfterLiquidate(env e, Midnight.Market market, uint256 collat assert marketIsCreated(market); } -// Markets can only be created by: touchMarket, take, withdraw, repay, supplyCollateral, withdrawCollateral or liquidate. +rule marketIsCreatedAfterClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver) { + claimContinuousFee(e, market, amount, receiver); + assert marketIsCreated(market); +} + +rule marketIsCreatedAfterUpdatePosition(env e, Midnight.Market market, address user) { + updatePosition(e, market, user); + assert marketIsCreated(market); +} + +// Markets can only be created by: touchMarket, take, withdraw, repay, supplyCollateral, withdrawCollateral, liquidate, claimContinuousFee or updatePosition. rule onlyTouchMarketCreatesMarket(env e, method f, calldataarg args, Midnight.Market market) filtered { f -> f.selector != sig:touchMarket(Midnight.Market).selector @@ -140,6 +150,8 @@ filtered { && f.selector != sig:supplyCollateral(Midnight.Market, uint256, uint256, 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:claimContinuousFee(Midnight.Market, uint256, address).selector + && f.selector != sig:updatePosition(Midnight.Market, address).selector } { require !marketIsCreated(market), "Assume that the market is not created"; f(e, args); diff --git a/certora/specs/LossFactor.spec b/certora/specs/LossFactor.spec index beebf46be..2eef29114 100644 --- a/certora/specs/LossFactor.spec +++ b/certora/specs/LossFactor.spec @@ -15,6 +15,7 @@ methods { // Deterministic toId needed to link market arguments to stored state. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // SafeTransferLib summaries: bypass transfer logic (needed for liquidate @withrevert rules). diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index bbc70600e..db72e9569 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -273,7 +273,7 @@ rule feeClaimerCanClaimSettlementFee(env e, address token, uint256 amount, addre rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver, address user) { bytes32 id = Utils.toId(market); address feeClaimerBefore = feeClaimer(); - bool marketIsCreated = marketIsCreated(id); + bool marketWasCreated = marketIsCreated(id); uint256 withdrawableBefore = withdrawable(id); uint256 totalUnitsBefore = totalUnits(id); uint128 continuousFeeCreditBefore = currentContract.marketState[id].continuousFeeCredit; @@ -283,7 +283,9 @@ rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amou claimContinuousFee@withrevert(e, market, amount, receiver); bool reverted = lastReverted; - assert !reverted <=> e.msg.sender == feeClaimerBefore && e.msg.value == 0 && marketIsCreated && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore; + assert !reverted => e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore; + assert !reverted && !marketWasCreated => amount == 0; + assert marketWasCreated && e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore => !reverted; assert !reverted => withdrawable(id) == withdrawableBefore - amount; assert !reverted => totalUnits(id) == totalUnitsBefore - amount; assert !reverted => currentContract.marketState[id].continuousFeeCredit == continuousFeeCreditBefore - amount; diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index d13808672..8463fac6c 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -43,6 +43,12 @@ invariant marketSettlementFeePerIndexBound(bytes32 id, uint256 index) preserved touchMarket(Midnight.Market market) with (env e) { requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); } + preserved claimContinuousFee(Midnight.Market market, uint256 amount, address receiver) with (env e) { + requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); + } + preserved updatePosition(Midnight.Market market, address user) with (env e) { + requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); + } preserved withdraw(Midnight.Market market, uint256 units, address onBehalf, address receiver) with (env e) { requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); } From 063920f9aa0bebe4070431fcfdb4740d22d8300a Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:30:49 +0200 Subject: [PATCH 20/29] fix(certora): relax continuous fee claim liveness --- certora/specs/Role.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index db72e9569..5ccaba4a1 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -284,7 +284,6 @@ rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amou claimContinuousFee@withrevert(e, market, amount, receiver); bool reverted = lastReverted; assert !reverted => e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore; - assert !reverted && !marketWasCreated => amount == 0; assert marketWasCreated && e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore => !reverted; assert !reverted => withdrawable(id) == withdrawableBefore - amount; assert !reverted => totalUnits(id) == totalUnitsBefore - amount; From adf466796eb351117a99f8bb15011f7710f76f9c Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:44:48 +0200 Subject: [PATCH 21/29] Apply suggestion from @MathisGD Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Midnight.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index df2b4d1df..1807f467f 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,7 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Market ids are scoped by market.chainId; identical markets on chains sharing a chain id may share ids. +/// @dev Markets contain the chainId of when they were created. This means that after a hard-fork, the resulting two markets on the two chains share the same id. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). From 31af161c28215a6f27d970dad82a51ceb4acbc03 Mon Sep 17 00:00:00 2001 From: "prd-carapulse[bot]" <264278285+prd-carapulse[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:50:55 +0200 Subject: [PATCH 22/29] style(midnight): format fork market id note --- src/Midnight.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index 1807f467f..1a906f3cd 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,7 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Markets contain the chainId of when they were created. This means that after a hard-fork, the resulting two markets on the two chains share the same id. +/// @dev Markets use their creation chain id, so the two post-fork markets share the same id. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). From d9f2e935dbacf398c17e663db9ba94e34f517f0c Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:28:07 +0200 Subject: [PATCH 23/29] Update src/Midnight.sol Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Midnight.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index 1a906f3cd..ca1e8e88c 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,7 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Markets use their creation chain id, so the two post-fork markets share the same id. +/// @dev Markets use their creation chain id, so after a chain fork, two markets on different chains can have the same id. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). From bdf11a34bb26c9f1749d9bed377def9b9f83173e Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:34:41 +0200 Subject: [PATCH 24/29] Apply suggestion from @MathisGD Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/Midnight.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Midnight.sol b/src/Midnight.sol index ca1e8e88c..1bdbebe63 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,7 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @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 Markets use their creation chain id, so after a chain fork, two markets on different chains can have the same id. +/// @dev Markets use their creation chainId, so after a chain fork two markets on different chains can have the same id. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). From 9b92e75d7f7af90d7a34af7194656548230ab2ed Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:05:58 +0200 Subject: [PATCH 25/29] Update certora/specs/TakeAmountsLibInvertibility.spec Co-authored-by: Quentin Garchery Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- certora/specs/TakeAmountsLibInvertibility.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/TakeAmountsLibInvertibility.spec b/certora/specs/TakeAmountsLibInvertibility.spec index 4ee3105be..9d02fb0dd 100644 --- a/certora/specs/TakeAmountsLibInvertibility.spec +++ b/certora/specs/TakeAmountsLibInvertibility.spec @@ -13,7 +13,7 @@ methods { function TakeAmountsLibHarness.buyerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); function TakeAmountsLibHarness.sellerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); - // Deterministic id: same Market => same id. Matches what take() and TakeAmountsLib observe (both reach IdLib.toId via IdLib.toId / Midnight.touchMarket). + // Deterministic toId function. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic price: the same tick yields the same price for both the library and take(). From 688c6562ddf0998b69af02b97a76c533ba13521e Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:07:25 +0200 Subject: [PATCH 26/29] Update src/libraries/IdLib.sol Co-authored-by: Quentin Garchery Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/libraries/IdLib.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index 972b1457e..5f6348ffb 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -33,7 +33,7 @@ library IdLib { ); } - /// @dev Stores the market in the code of the contract at an address equal to the last 20 bytes of its id. + /// @dev Stores the market in the code of the contract at the address given by the last 20 bytes of its id. function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { From a66ab06b224f4304a482049942a3b155a742b605 Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:07:53 +0200 Subject: [PATCH 27/29] Update src/interfaces/IMidnight.sol Co-authored-by: Quentin Garchery Signed-off-by: MathisGD <74971347+MathisGD@users.noreply.github.com> --- src/interfaces/IMidnight.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 4f1dd5923..ff7e5acf4 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -78,8 +78,8 @@ interface IMidnight { error ContinuousFeeAboveOfferCap(); error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); - error InvalidFeeIndex(); error InvalidChainId(); + error InvalidFeeIndex(); error InvalidLltv(); error InvalidMaxLif(); error InvalidMidnight(); From fa2ce39203f133247826ddaf9d3832757c09f49e Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:14:55 +0200 Subject: [PATCH 28/29] test --- test/IdLibTest.sol | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index 2c6d587b4..909afce5b 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -20,23 +20,17 @@ contract IdLibTest is Test { market.liquidatorGate = address(6); } - function testToIdIsSensitiveToChainId() public pure { + function testToIdIsSensitiveToMarketParams() public pure { Market memory market1 = baseMarket(); Market memory market2 = baseMarket(); market2.chainId = 2; assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - } - function testToIdIsSensitiveToMidnight() public pure { - Market memory market1 = baseMarket(); - Market memory market2 = baseMarket(); + market2 = baseMarket(); market2.midnight = address(7); assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - } - function testToIdIsSensitiveToMarketParams() public pure { - Market memory market1 = baseMarket(); - Market memory market2 = baseMarket(); + market2 = baseMarket(); market2.loanToken = address(8); assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); @@ -44,6 +38,18 @@ contract IdLibTest is Test { market2.collateralParams[0].token = address(9); assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + market2 = baseMarket(); + market2.collateralParams[0].lltv = 0.5e18; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].maxLif = 1.2e18; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].oracle = address(12); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + market2 = baseMarket(); market2.maturity = 789; assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); From df4128524f8d2d5086107cc9194b5ce568aa85eb Mon Sep 17 00:00:00 2001 From: MathisGD <74971347+MathisGD@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:29:46 +0200 Subject: [PATCH 29/29] remove touchmarket summary --- certora/specs/LiquidationBoundedByLIF.spec | 4 ++-- certora/specs/LiquidationProfitability.spec | 4 ++-- certora/specs/LossFactor.spec | 1 - certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec | 4 ++-- certora/specs/SplitPreservesAccounting.spec | 4 ++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/certora/specs/LiquidationBoundedByLIF.spec b/certora/specs/LiquidationBoundedByLIF.spec index 5874e4cca..b8376cc74 100644 --- a/certora/specs/LiquidationBoundedByLIF.spec +++ b/certora/specs/LiquidationBoundedByLIF.spec @@ -16,8 +16,8 @@ methods { // Deterministic toId summary using a wrapper that extracts all scalar Market fields. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Skip market creation logic: removes the collateral-validation loop. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Token transfers happen after return values are computed; irrelevant to the assertion. function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/LiquidationProfitability.spec b/certora/specs/LiquidationProfitability.spec index 52b8794a4..64334730f 100644 --- a/certora/specs/LiquidationProfitability.spec +++ b/certora/specs/LiquidationProfitability.spec @@ -17,8 +17,8 @@ methods { // Deterministic toId summary using a wrapper that extracts all scalar Market fields. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Skip market creation logic: removes the collateral-validation loop. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Token transfers happen after return values are computed; irrelevant to the assertion. function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/LossFactor.spec b/certora/specs/LossFactor.spec index 2eef29114..beebf46be 100644 --- a/certora/specs/LossFactor.spec +++ b/certora/specs/LossFactor.spec @@ -15,7 +15,6 @@ methods { // Deterministic toId needed to link market arguments to stored state. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // SafeTransferLib summaries: bypass transfer logic (needed for liquidate @withrevert rules). diff --git a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec index 3d3d993a5..49a45af29 100644 --- a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec +++ b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec @@ -15,8 +15,8 @@ methods { // Summarize toId: deterministic hash preserves market-to-id relationship without adding assumptions. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Assume that the markets are already created. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Pure helper called with identical args across the three takes; CONSTANT collapses // its bit / hashing / arithmetic complexity (no behavioral abstraction). diff --git a/certora/specs/SplitPreservesAccounting.spec b/certora/specs/SplitPreservesAccounting.spec index 8ed4783ca..715cad6c9 100644 --- a/certora/specs/SplitPreservesAccounting.spec +++ b/certora/specs/SplitPreservesAccounting.spec @@ -20,8 +20,8 @@ methods { // Deterministic hash preserves market-to-id relationship without adding assumptions. function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Assume that the markets are already created. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Pure helper called with identical args across the three takes; CONSTANT collapses // its bit / hashing / arithmetic complexity (no behavioral abstraction).