diff --git a/certora/README.md b/certora/README.md index c4ff2a4f..c5f29331 100644 --- a/certora/README.md +++ b/certora/README.md @@ -13,11 +13,11 @@ Global invariants on positions, markets and accounting. Total units always equal the sum of debt plus withdrawable, a user never holds both credit and debt, and a position's pending continuous fee never exceeds its credit. Continuous fees stay below `MAX_CONTINUOUS_FEE` at both the default and the market level, and loss factors only ever increase, with each user's bounded by its market's. Rules also pin down `take`/`liquidate` input-output consistency: zero inputs give zero outputs, and `take` raises the claimable settlement fee by exactly the buyer/seller spread. - It also shows that neither credit nor debt can grow once a market's loss factor is maxed out. + It also shows that neither credit nor debt can grow once a market's loss factor is maxed out, and that every enabled `LLTV` tier and liquidation cursor is at most `WAD`. - [`BalanceEffects.spec`](specs/BalanceEffects.spec) pins down the exact credit, debt and collateral effect of every entry point. - [`WithdrawableMonotonicity.spec`](specs/WithdrawableMonotonicity.spec) checks how withdrawable assets move: up on `repay` and `liquidate`, down by exactly the amount on `withdraw` and `claimContinuousFee`, and unchanged otherwise. It checks the claimable settlement fee the same way: up on `take`, down on `claimSettlementFee`, and unchanged otherwise. -- [`CreatedMarkets.spec`](specs/CreatedMarkets.spec) checks the well-formedness invariants of a created market: a non-empty collateral list, strictly sorted by token, with no zero token, and every entry with an `LLTV <= WAD` from an allowed tier and an allowed `maxLif`. +- [`CreatedMarkets.spec`](specs/CreatedMarkets.spec) checks the well-formedness invariants of a created market: a non-empty collateral list, strictly sorted by token, with no zero token, and every entry with an enabled `LLTV` tier, an enabled liquidation cursor, and a `maxLif <= 2 * WAD`. Rules add that a market is created by the first interaction of each entry point, can only be created that way, and can never be deleted. - [`NotCreatedMarket.spec`](specs/NotCreatedMarket.spec) checks the converse: every state field of a market that was never created is empty. - [`LossFactor.spec`](specs/LossFactor.spec) checks that only `liquidate` changes a market's loss factor, and only when bad debt is realized (total units decrease), and that `updatePosition` syncs the user's `lastLossFactor` to the market's. diff --git a/certora/confs/CreatedMarkets.conf b/certora/confs/CreatedMarkets.conf index f6ff64b7..8ff15755 100644 --- a/certora/confs/CreatedMarkets.conf +++ b/certora/confs/CreatedMarkets.conf @@ -24,8 +24,9 @@ "-s [z3:def{randomSeed=1},z3:def{randomSeed=2},z3:def{randomSeed=3},z3:def{randomSeed=4},z3:def{randomSeed=5},z3:def{randomSeed=6},z3:def{randomSeed=7},z3:def{randomSeed=8},z3:def{randomSeed=9},z3:def{randomSeed=10}]" ], "split_rules": [ - "createdMarketsHaveAllowedMaxLif", - "createdMarketsHaveAllowedLltv" + "createdMarketsHaveEnabledLltv", + "createdMarketsHaveEnabledLiquidationCursor", + "createdMarketsHaveMaxLifAtMostTwoWad" ], "msg": "CreatedMarkets" } diff --git a/certora/confs/TakeAmountsLibInvertibility.conf b/certora/confs/TakeAmountsLibInvertibility.conf index 15507fa1..a42d09f0 100644 --- a/certora/confs/TakeAmountsLibInvertibility.conf +++ b/certora/confs/TakeAmountsLibInvertibility.conf @@ -13,9 +13,10 @@ "optimistic_loop": true, "loop_iter": 2, "prover_args": [ - "-depth 5", - "-mediumTimeout 60", - "-timeout 3600" + "-destructiveOptimizations twostage", + "-splitParallel true", + "-splitParallelInitialDepth 4", + "-splitParallelTimelimit 7200" ], "msg": "Midnight TakeAmountsLib Invertibility" } diff --git a/certora/helpers/Utils.sol b/certora/helpers/Utils.sol index af7b56e9..41d59abf 100644 --- a/certora/helpers/Utils.sol +++ b/certora/helpers/Utils.sol @@ -6,8 +6,6 @@ import {Offer, Market} from "../../src/interfaces/IMidnight.sol"; import {UtilsLib} from "../../src/libraries/UtilsLib.sol"; import { CALLBACK_SUCCESS, - LIQUIDATION_CURSOR_LOW, - LIQUIDATION_CURSOR_HIGH, MAX_COLLATERALS_PER_BORROWER, maxSettlementFee as _maxSettlementFee, maxLif as _maxLif @@ -51,16 +49,8 @@ contract Utils { return _maxSettlementFee(index); } - function maxLif(uint256 lltv, uint256 cursor) external pure returns (uint256) { - return _maxLif(lltv, cursor); - } - - function liquidationCursorLow() external pure returns (uint256) { - return LIQUIDATION_CURSOR_LOW; - } - - function liquidationCursorHigh() external pure returns (uint256) { - return LIQUIDATION_CURSOR_HIGH; + function maxLif(uint256 lltv, uint256 liquidationCursor) external pure returns (uint256) { + return _maxLif(lltv, liquidationCursor); } function maxCollateralsPerBorrower() external pure returns (uint256) { diff --git a/certora/specs/CreatedMarkets.spec b/certora/specs/CreatedMarkets.spec index 62000bf7..9806cdac 100644 --- a/certora/specs/CreatedMarkets.spec +++ b/certora/specs/CreatedMarkets.spec @@ -6,11 +6,9 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; function tickSpacing(bytes32) external returns (uint8) envfree; - function isLltvAllowed(uint256) external returns (bool) envfree; + function isLltvEnabled(uint256) external returns (bool) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function Utils.maxLif(uint256, uint256) external returns (uint256) envfree; - function Utils.liquidationCursorLow() external returns (uint256) envfree; - function Utils.liquidationCursorHigh() external returns (uint256) envfree; // Over-approximate view functions for prover performance. function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; @@ -49,8 +47,6 @@ function marketIsCreated(Midnight.Market market) returns (bool) { return tickSpacing(summaryToId(market)) > 0; } -definition isMaxLifAllowed(uint256 lltv, uint256 maxLif) returns bool = maxLif == Utils.maxLif(lltv, Utils.liquidationCursorLow()) || maxLif == Utils.maxLif(lltv, Utils.liquidationCursorHigh()); - /// RULES /// // Show that a created market has at least one collateral. @@ -65,26 +61,15 @@ strong invariant createdMarketsHaveSortedCollaterals(Midnight.Market market, uin strong invariant createdMarketsHaveNonZeroCollaterals(Midnight.Market market, uint256 i) marketIsCreated(market) => i < market.collateralParams.length => market.collateralParams[i].token != 0; -// Show that an allowed LLTV tier is at most WAD, which holds because tiers can only be added with lltv <= WAD. -strong invariant allowedLltvIsLessThanOrEqualToOne(uint256 lltv) - isLltvAllowed(lltv) => lltv <= WAD(); - -// Show that a created market has lltv <= WAD. -strong invariant createdMarketsHaveLltvLessThanOrEqualToOne(Midnight.Market market, uint256 i) - marketIsCreated(market) => i < market.collateralParams.length => market.collateralParams[i].lltv <= WAD() - { - preserved { - requireInvariant allowedLltvIsLessThanOrEqualToOne(market.collateralParams[i].lltv); - } - } - -// Show that a created market only has allowed LLTV tiers. -strong invariant createdMarketsHaveAllowedLltv(Midnight.Market market, uint256 i) - marketIsCreated(market) => i < market.collateralParams.length => isLltvAllowed(market.collateralParams[i].lltv); - -// Show that a created market has maxLif allowed. -strong invariant createdMarketsHaveAllowedMaxLif(Midnight.Market market, uint256 i) - marketIsCreated(market) => i < market.collateralParams.length => isMaxLifAllowed(market.collateralParams[i].lltv, market.collateralParams[i].maxLif); +// Show that a created market only has enabled LLTV tiers. +strong invariant createdMarketsHaveEnabledLltv(Midnight.Market market, uint256 i) + marketIsCreated(market) => i < market.collateralParams.length => isLltvEnabled(market.collateralParams[i].lltv); + +strong invariant createdMarketsHaveEnabledLiquidationCursor(Midnight.Market market, uint256 i) + marketIsCreated(market) => i < market.collateralParams.length => currentContract.isLiquidationCursorEnabled[market.collateralParams[i].liquidationCursor]; + +strong invariant createdMarketsHaveMaxLifAtMostTwoWad(Midnight.Market market, uint256 i) + marketIsCreated(market) => i < market.collateralParams.length => Utils.maxLif(market.collateralParams[i].lltv, market.collateralParams[i].liquidationCursor) <= 2 * WAD(); // Show that a created market cannot be deleted. rule marketCannotBeDeleted(env e, method f, calldataarg args, Midnight.Market market) { diff --git a/certora/specs/ExactMath.spec b/certora/specs/ExactMath.spec index b66f4823..2c2fbb47 100644 --- a/certora/specs/ExactMath.spec +++ b/certora/specs/ExactMath.spec @@ -9,28 +9,14 @@ methods { definition WAD() returns uint256 = 10 ^ 18; -rule lifTimesLltvIsLessThanOrEqualToOne(uint256 lltv, uint256 cursor) { - require lltv <= WAD(), "see rule createdMarketsHaveLltvLessThanOrEqualToOne"; - require cursor < WAD(), "see the definition of LIQUIDATION_CURSOR_LOW and LIQUIDATION_CURSOR_HIGH"; - assert lltv * maxLif(lltv, cursor) <= WAD() * WAD(); +rule lifTimesLltvIsLessThanOrEqualToOne(uint256 lltv, uint256 liquidationCursor) { + require lltv <= WAD(), "see rule createdMarketsHaveEnabledLltv"; + require liquidationCursor <= WAD(), "enabled liquidationCursors are at most WAD, see addLiquidationCursor"; + assert lltv * maxLif(lltv, liquidationCursor) <= WAD() * WAD(); } -/// Check that maxLif >= WAD -rule maxLifIsAtLeastWad(uint256 lltv, uint256 cursor) { - assert maxLif(lltv, cursor) >= WAD(); -} - -/// Check that maxLif <= 2*WAD for valid cursor values -rule maxLifIsAtMostTwoWad(uint256 lltv, uint256 cursor) { - require lltv <= WAD(), "see rule createdMarketsHaveLltvLessThanOrEqualToOne"; - require cursor <= WAD() / 2, "see LIQUIDATION_CURSOR_HIGH in ConstantsLib"; - assert maxLif(lltv, cursor) <= 2 * WAD(); -} - -/// Check that maxLif * lltv <= WAD * (WAD - 1) for valid cursor values -rule lifTimesLltvStrictBound(uint256 lltv, uint256 cursor) { - require cursor < WAD(), "see the definition of LIQUIDATION_CURSOR_LOW and LIQUIDATION_CURSOR_HIGH"; - assert lltv < WAD() => lltv * maxLif(lltv, cursor) <= WAD() * (WAD() - 1); +rule maxLifIsAtLeastWad(uint256 lltv, uint256 liquidationCursor) { + assert maxLif(lltv, liquidationCursor) >= WAD(); } /// Check that mulDivUp(a, lltv, WAD()) <= mulDivUp(a, WAD(), lif) diff --git a/certora/specs/Healthiness.spec b/certora/specs/Healthiness.spec index bdf41b04..df2ba1de 100644 --- a/certora/specs/Healthiness.spec +++ b/certora/specs/Healthiness.spec @@ -23,6 +23,10 @@ methods { // The axioms are proved in MulDiv.spec. function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivDown(x, y, d); function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivUp(x, y, d); + + // maxLif is recomputed on the fly from (lltv, liquidationCursor) during liquidate. Summarize it by a deterministic ghost; its + // lltv * maxLif <= WAD * WAD bound is assumed below (see lifTimesLltvIsLessThanOrEqualToOne in ExactMath.spec). + function maxLif(uint256 lltv, uint256 liquidationCursor) internal returns (uint256) => maxLifGhost(lltv, liquidationCursor); function _.havocAll() external => HAVOC_ALL; function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; @@ -110,7 +114,9 @@ persistent ghost mapping(uint256 => address) globalMarketCollateralToken; persistent ghost mapping(uint256 => uint256) globalMarketCollateralLLTV; -persistent ghost mapping(uint256 => uint256) globalMarketCollateralMaxLif; +persistent ghost mapping(uint256 => uint256) globalMarketCollateralLiquidationCursor; + +persistent ghost maxLifGhost(uint256, uint256) returns uint256; persistent ghost uint256 globalMarketMaturity; @@ -126,7 +132,7 @@ persistent ghost address globalBorrower; // helper function to check if one of the collateralParams of a market matches the global variables. // It checks for the length and also returns true if the index is out of bounds. This allows us to require this for every index. -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]); +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].liquidationCursor == globalMarketCollateralLiquidationCursor[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; @@ -241,7 +247,7 @@ rule stayHealthyLiquidateSameBorrower(env e, uint256 collateralIndex, uint256 se // This variable is set to false whenever isHealthy() is violated before a callback. Initially we set it to true to indicate no violations detected. healthyOrLockedBeforeCallbacks = true; - require globalMarketCollateralLLTV[collateralIndex] * globalMarketCollateralMaxLif[collateralIndex] <= WAD() * WAD(), "Proved in lifTimesLltvIsLessThanOrEqualToOne in ExactMath.spec: maxLif is at most 1/lltv"; + require globalMarketCollateralLLTV[collateralIndex] * maxLifGhost(globalMarketCollateralLLTV[collateralIndex], globalMarketCollateralLiquidationCursor[collateralIndex]) <= WAD() * WAD(), "Proved in lifTimesLltvIsLessThanOrEqualToOne in ExactMath.spec: maxLif is at most 1/lltv"; require globalMarketCollateralLength <= 2, "too many collateralParams for the spec to handle"; @@ -266,9 +272,9 @@ rule stayHealthyLiquidateSameBorrower(env e, uint256 collateralIndex, uint256 se require forall mathint a. forall mathint b. forall mathint d1. forall mathint d2. axiomUpMonotoneD(a, b, d1, d2), "axiom"; require axiomDownZero(price, ORACLE_PRICE_SCALE()), "axiom"; require axiomDownZero(globalMarketCollateralLLTV[collateralIndex], WAD()), "axiom"; - require axiomInverseUpDown(repaidUnitsOut, globalMarketCollateralMaxLif[collateralIndex], WAD()), "axiom"; - require axiomInverseUpDown(ghostMulDivDown(repaidUnitsOut, globalMarketCollateralMaxLif[collateralIndex], WAD()), ORACLE_PRICE_SCALE(), price), "axiom"; - require axiomLifLLTV(ghostMulDivUp(seizedAssetsOut, price, ORACLE_PRICE_SCALE()), globalMarketCollateralMaxLif[collateralIndex], globalMarketCollateralLLTV[collateralIndex]), "axiom"; + require axiomInverseUpDown(repaidUnitsOut, maxLifGhost(globalMarketCollateralLLTV[collateralIndex], globalMarketCollateralLiquidationCursor[collateralIndex]), WAD()), "axiom"; + require axiomInverseUpDown(ghostMulDivDown(repaidUnitsOut, maxLifGhost(globalMarketCollateralLLTV[collateralIndex], globalMarketCollateralLiquidationCursor[collateralIndex]), WAD()), ORACLE_PRICE_SCALE(), price), "axiom"; + require axiomLifLLTV(ghostMulDivUp(seizedAssetsOut, price, ORACLE_PRICE_SCALE()), maxLifGhost(globalMarketCollateralLLTV[collateralIndex], globalMarketCollateralLiquidationCursor[collateralIndex]), globalMarketCollateralLLTV[collateralIndex]), "axiom"; require axiomAddDownUp(collateralAfter, seizedAssetsOut, price, ORACLE_PRICE_SCALE()), "axiom"; require axiomAddDownUp(ghostMulDivDown(collateralAfter, price, ORACLE_PRICE_SCALE()), ghostMulDivUp(seizedAssetsOut, price, ORACLE_PRICE_SCALE()), globalMarketCollateralLLTV[collateralIndex], WAD()), "axiom"; diff --git a/certora/specs/LiquidationBoundedByLIF.spec b/certora/specs/LiquidationBoundedByLIF.spec index efd665e9..5f1730ac 100644 --- a/certora/specs/LiquidationBoundedByLIF.spec +++ b/certora/specs/LiquidationBoundedByLIF.spec @@ -9,6 +9,7 @@ methods { function collateral(bytes32 id, address user, uint256 index) external returns (uint128) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; + function Utils.maxLif(uint256, uint256) external returns (uint256) envfree; // Summary to capture the oracle price so the spec can reference it in assertions. function _.price() external => summaryPrice(calledContract) expect(uint256); @@ -76,7 +77,7 @@ strong invariant nonZeroCollateralsAreActivated(bytes32 id, address user, uint25 /// Liquidation profit is bounded by maxLif (repaidUnits input). /// Unlike the seizedAssets rule, no requireInvariant is needed here: if collateralIndex is not in the bitmap because mulDivDown(..., 0) reverts. rule liquidationProfitBoundedInputRepaidUnits(env e, Midnight.Market market, uint256 collateralIndex, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - mathint maxLif = market.collateralParams[collateralIndex].maxLif; + mathint maxLif = Utils.maxLif(market.collateralParams[collateralIndex].lltv, market.collateralParams[collateralIndex].liquidationCursor); require data.length == 0, "no callback for prover performance"; require maxLif >= WAD(), "maxLif must be at least 1x for profit boundedness (see touchMarket validation and ExactMath.spec)"; @@ -91,7 +92,7 @@ rule liquidationProfitBoundedInputRepaidUnits(env e, Midnight.Market market, uin /// Liquidation profit is bounded by maxLif (seizedAssets input) rule liquidationProfitBoundedSeizedAssets(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - mathint maxLif = market.collateralParams[collateralIndex].maxLif; + mathint maxLif = Utils.maxLif(market.collateralParams[collateralIndex].lltv, market.collateralParams[collateralIndex].liquidationCursor); require data.length == 0, "no callback for prover performance"; require maxLif >= WAD(), "maxLif must be at least 1x for profit boundedness (see touchMarket validation and ExactMath.spec)"; diff --git a/certora/specs/LiquidationProfitability.spec b/certora/specs/LiquidationProfitability.spec index 220e1067..0c2951f8 100644 --- a/certora/specs/LiquidationProfitability.spec +++ b/certora/specs/LiquidationProfitability.spec @@ -6,6 +6,7 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; + function Utils.maxLif(uint256, uint256) external returns (uint256) envfree; // Summary to capture the oracle price so the spec can reference it in assertions. function _.price() external => summaryPrice(calledContract) expect(uint256); @@ -69,7 +70,7 @@ function summaryMulDivUp(uint256 a, uint256 b, uint256 d) returns uint256 { /// For repaidUnits input: lif >= WAD (solvency), and lif == maxLif when in normal mode or when the call is >= 60 min post-maturity (profitability). rule liquidationLifRepaidUnits(env e, Midnight.Market market, uint256 collateralIndex, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - uint256 maxLif = market.collateralParams[collateralIndex].maxLif; + uint256 maxLif = Utils.maxLif(market.collateralParams[collateralIndex].lltv, market.collateralParams[collateralIndex].liquidationCursor); require maxLif >= WAD(), "see the rule maxLifIsAtLeastWad"; bool maxLifReached = !postMaturityMode || e.block.timestamp >= require_uint256(market.maturity + TIME_TO_MAX_LIF()); @@ -89,7 +90,7 @@ rule liquidationLifRepaidUnits(env e, Midnight.Market market, uint256 collateral /// For seizedAssets input: lif >= WAD (solvency), and lif == maxLif when in normal mode or when the call is >= 60 min post-maturity (profitability). rule liquidationLifSeizedAssets(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - uint256 maxLif = market.collateralParams[collateralIndex].maxLif; + uint256 maxLif = Utils.maxLif(market.collateralParams[collateralIndex].lltv, market.collateralParams[collateralIndex].liquidationCursor); require maxLif >= WAD(), "see the rule maxLifIsAtLeastWad"; bool maxLifReached = !postMaturityMode || e.block.timestamp >= require_uint256(market.maturity + TIME_TO_MAX_LIF()); diff --git a/certora/specs/Midnight.spec b/certora/specs/Midnight.spec index a8731d84..29cc6d83 100644 --- a/certora/specs/Midnight.spec +++ b/certora/specs/Midnight.spec @@ -36,6 +36,8 @@ definition MAX_CONTINUOUS_FEE() returns uint256 = 317097919; definition MAX_TTM() returns mathint = 100 * 365 * 86400; +definition WAD() returns uint256 = 10 ^ 18; + function summaryToId(Midnight.Market market) returns (bytes32) { return Utils.hashMarket(market); } @@ -159,3 +161,9 @@ strong invariant lastLossFactorLeqMarketLossFactor(bytes32 id, address user) /// A user cannot have both credit and debt. strong invariant noCreditAndDebt(bytes32 id, address user) credit(id, user) == 0 || debt(id, user) == 0; + +strong invariant enabledLltvIsLessThanOrEqualToOne(uint256 lltv) + currentContract.isLltvEnabled[lltv] => lltv <= WAD(); + +strong invariant enabledLiquidationCursorsAreBounded(uint256 liquidationCursor) + currentContract.isLiquidationCursorEnabled[liquidationCursor] => liquidationCursor <= WAD(); diff --git a/certora/specs/NoDivisionByZero.spec b/certora/specs/NoDivisionByZero.spec index 1eb8c1c5..411e0dd4 100644 --- a/certora/specs/NoDivisionByZero.spec +++ b/certora/specs/NoDivisionByZero.spec @@ -27,6 +27,11 @@ methods { // Hook on mulDivDown and mulDivUp to check that the denominator is not zero, and add the necessary lemmas. function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivDownSummary(x, y, d); function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivUpSummary(x, y, d); + + // maxLif is recomputed on the fly from (lltv, liquidationCursor) during liquidate. Summarize it by a deterministic ghost: its + // value bounds are assumed below (see ExactMath.spec), and its own well-definedness (no division by zero for valid + // liquidationCursors) is proven there too. + function maxLif(uint256 lltv, uint256 liquidationCursor) internal returns (uint256) => maxLifGhost(lltv, liquidationCursor); } /// GHOSTS /// @@ -43,7 +48,9 @@ persistent ghost mapping(uint256 => address) globalMarketCollateralToken; persistent ghost mapping(uint256 => uint256) globalMarketCollateralLLTV; -persistent ghost mapping(uint256 => uint256) globalMarketCollateralMaxLif; +persistent ghost mapping(uint256 => uint256) globalMarketCollateralLiquidationCursor; + +persistent ghost maxLifGhost(uint256, uint256) returns uint256; persistent ghost uint256 globalMarketMaturity; @@ -68,7 +75,7 @@ ghost ghostPrice(address) returns uint256; 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]); +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].liquidationCursor == globalMarketCollateralLiquidationCursor[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; @@ -116,9 +123,7 @@ rule noDivisionByZeroLiquidate(env e, Midnight.Market market, uint256 collateral require equalsGlobalMarket(market); // Needed for the bitmap loop which calls mulDivUp(WAD, maxLif) for every activated collateral. - require forall uint256 i. i < market.collateralParams.length => market.collateralParams[i].maxLif >= WAD(), "see maxLifIsAtLeastWad in ExactMath.spec"; - - require market.collateralParams[collateralIndex].lltv < WAD() => to_mathint(market.collateralParams[collateralIndex].maxLif) * to_mathint(market.collateralParams[collateralIndex].lltv) <= to_mathint(WAD()) * (to_mathint(WAD()) - 1), "see lifTimesLltvStrictBound in ExactMath.spec"; + require forall uint256 i. i < market.collateralParams.length => maxLifGhost(market.collateralParams[i].lltv, market.collateralParams[i].liquidationCursor) >= WAD(), "see maxLifIsAtLeastWad in ExactMath.spec"; // Assume that the collateral price is non-zero and the collateral is active. Otherwise, liquidate may revert with div by zero. require ghostPrice(market.collateralParams[collateralIndex].oracle) > 0, "Assumption: the collateral price is not zero"; diff --git a/certora/specs/NoMultiplicationOverflow.spec b/certora/specs/NoMultiplicationOverflow.spec index 51ba4fd2..59547177 100644 --- a/certora/specs/NoMultiplicationOverflow.spec +++ b/certora/specs/NoMultiplicationOverflow.spec @@ -22,22 +22,29 @@ methods { // Summarize mulDivDown and mulDivUp to track overflow. function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivDownSummary(x, y, d); function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => mulDivUpSummary(x, y, d); + + // maxLif is recomputed on the fly from (lltv, liquidationCursor) during liquidate. Summarize it by a deterministic ghost so + // its (bounded) value can be assumed under a quantifier; its own arithmetic is safe and bounded for valid liquidationCursors + // (see ExactMath.spec: maxLifIsAtLeastWad, maxLifIsAtMostTwoWad). + function maxLif(uint256 lltv, uint256 liquidationCursor) internal returns (uint256) => maxLifGhost(lltv, liquidationCursor); } /// HELPERS /// persistent ghost bool mulOverflow; +persistent ghost maxLifGhost(uint256, uint256) returns uint256; + definition WAD() returns uint256 = 10 ^ 18; definition ORACLE_PRICE_SCALE() returns uint256 = 10 ^ 36; -// Proven in CreatedMarkets.spec (createdMarketsHaveLltvLessThanOrEqualToOne) +// Proven in CreatedMarkets.spec (createdMarketsHaveEnabledLltv) // and ExactMath.spec (maxLifIsAtLeastWad, maxLifIsAtMostTwoWad). // Maturity is bounded to uint64 as a realistic timestamp assumption for overflow analysis. function summaryToId(Midnight.Market market) returns (bytes32) { require forall uint256 i. i < market.collateralParams.length => market.collateralParams[i].lltv <= WAD(), "proven in CreatedMarkets.spec"; - require forall uint256 i. i < market.collateralParams.length => market.collateralParams[i].maxLif >= WAD() && market.collateralParams[i].maxLif <= 2 * WAD(), "proven in ExactMath.spec"; + require forall uint256 i. i < market.collateralParams.length => maxLifGhost(market.collateralParams[i].lltv, market.collateralParams[i].liquidationCursor) >= WAD() && maxLifGhost(market.collateralParams[i].lltv, market.collateralParams[i].liquidationCursor) <= 2 * WAD(), "proven in ExactMath.spec"; require market.maturity <= max_uint64, "maturity fits in uint64: realistic timestamp assumption"; return Utils.hashMarket(market); } diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index a8206c67..d0b179db 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -9,7 +9,7 @@ methods { function feeSetter() external returns (address) envfree; function feeClaimer() external returns (address) envfree; function tickSpacingSetter() external returns (address) envfree; - function isLltvAllowed(uint256 lltv) external returns (bool) envfree; + function isLltvEnabled(uint256 lltv) external returns (bool) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; function continuousFee(bytes32 id) external returns (uint32) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; @@ -93,7 +93,7 @@ rule roleSetterCanAddLltv(env e, uint256 lltv) { addLltv@withrevert(e, lltv); assert !lastReverted <=> e.msg.sender == roleSetterBefore && e.msg.value == 0 && lltv <= WAD(); - assert !lastReverted => isLltvAllowed(lltv); + assert !lastReverted => isLltvEnabled(lltv); } /// ROLE SETTER: ACCESS CONTROL /// @@ -133,16 +133,54 @@ rule onlyRoleSetterCanChangeTickSpacingSetter(env e, method f, calldataarg args) assert tickSpacingSetter() != tickSpacingSetterBefore => e.msg.sender == roleSetterBefore && f.selector == sig:setTickSpacingSetter(address).selector; } -/// Allowed LLTV tiers can only be added by the role setter, and never removed. +/// LLTV TIERS: ACCESS CONTROL /// + +/// Enabled LLTV tiers can only be added by the role setter, and never removed. rule onlyRoleSetterCanAddLltv(env e, method f, calldataarg args, uint256 lltv) filtered { f -> !f.isView } { - bool allowedBefore = isLltvAllowed(lltv); + bool enabledBefore = isLltvEnabled(lltv); + address roleSetterBefore = roleSetter(); + + f(e, args); + + assert isLltvEnabled(lltv) != enabledBefore => enabledBefore == false && e.msg.sender == roleSetterBefore && f.selector == sig:addLltv(uint256).selector; +} + +/// LIQUIDATION CURSORS: LIVENESS /// + +rule roleSetterCanAddLiquidationCursor(env e, uint256 liquidationCursor) { + address roleSetterBefore = roleSetter(); + + addLiquidationCursor@withrevert(e, liquidationCursor); + bool reverted = lastReverted; + assert !reverted <=> e.msg.sender == roleSetterBefore && e.msg.value == 0 && liquidationCursor <= WAD(); + assert !reverted => currentContract.isLiquidationCursorEnabled[liquidationCursor]; +} + +/// LIQUIDATION CURSORS: ACCESS CONTROL /// + +/// Only the role setter can enable a liquidationCursor, and only through addLiquidationCursor. +rule onlyRoleSetterCanAddLiquidationCursor(env e, method f, calldataarg args, uint256 liquidationCursor) filtered { f -> !f.isView } { + bool enabledBefore = currentContract.isLiquidationCursorEnabled[liquidationCursor]; address roleSetterBefore = roleSetter(); f(e, args); - assert isLltvAllowed(lltv) != allowedBefore => allowedBefore == false && e.msg.sender == roleSetterBefore && f.selector == sig:addLltv(uint256).selector; + assert currentContract.isLiquidationCursorEnabled[liquidationCursor] != enabledBefore => e.msg.sender == roleSetterBefore && f.selector == sig:addLiquidationCursor(uint256).selector; +} + +/// LiquidationCursors can only be enabled, never disabled. +rule liquidationCursorsOnlyGrow(env e, method f, calldataarg args, uint256 liquidationCursor) filtered { f -> !f.isView } { + bool enabledBefore = currentContract.isLiquidationCursorEnabled[liquidationCursor]; + + f(e, args); + + assert enabledBefore => currentContract.isLiquidationCursorEnabled[liquidationCursor]; } +/// Every enabled liquidationCursor is at most WAD. +invariant liquidationCursorsBoundedByOne(uint256 liquidationCursor) + currentContract.isLiquidationCursorEnabled[liquidationCursor] => liquidationCursor <= WAD(); + /// FEE SETTER: LIVENESS /// rule feeSetterCanSetMarketSettlementFee(env e, bytes32 id, uint256 index, uint256 newSettlementFee) { diff --git a/src/Midnight.sol b/src/Midnight.sol index 2f6fc6e7..d7fe7aff 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -65,11 +65,13 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev There are two liquidation modes: The "post-maturity mode", available after the market's maturity, and the /// "normal mode", available if the borrower is unhealthy. After maturity, an unhealthy borrower's liquidator can choose /// between both modes. -/// @dev In the "normal mode", the liquidation incentive factor (LIF) is maxLif and the liquidation amount is capped -/// by what is needed to put back the position into health ("recovery close factor", or "RCF"). +/// @dev In the "normal mode", the liquidation incentive factor (LIF) is the computed maxLif and the liquidation amount +/// is capped by what is needed to put back the position into health ("recovery close factor", or "RCF"). /// @dev The RCF condition is (omitting scaling and roundings): /// newDebt >= newMaxDebt <=> debt - repaidUnits >= maxDebt - repaidUnits*LIF*LLTV /// <=> repaidUnits <= (debt-maxDebt) / (1 - LIF*LLTV). +/// @dev When LIF*LLTV = 1, repaying never restores health, so the RCF is inactive and the whole position can be +/// liquidated. /// @dev The RCF is deactivated for small collateral amount, essentially to mitigate issues with liquidations that are /// too small compared to the gas cost. More precisely, it is deactivated if the liquidation could leave a collateral /// with a value that would not be enough to repay rcfThreshold units. Which means (omitting scaling and roundings): @@ -79,8 +81,8 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev Nothing prevents borrowers from opening small positions / liquidators from leaving small positions that might /// not be profitable to liquidate because of gas cost. The RCF deactivation at rcfThreshold just prevents the systemic /// aspect. -/// @dev In the "post-maturity mode", the LIF (liquidation incentive factor) grows linearly from 1 at maturity to maxLif -/// at maturity + TIME_TO_MAX_LIF, and the RCF is deactivated. +/// @dev In the "post-maturity mode", the LIF (liquidation incentive factor) grows linearly from 1 at maturity to the +/// computed maxLif at maturity + TIME_TO_MAX_LIF, and the RCF is deactivated. /// @dev In both modes, maxLif is used to determine if the account has some bad debt, to always assume the worst case. /// /// SLASHING @@ -166,7 +168,8 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// revert. /// /// ROLES -/// @dev The role setter can set the role setter, fee setter, fee claimer, and tick spacing setter, and add LLTV tiers. +/// @dev The role setter can set the role setter, fee setter, fee claimer, and tick spacing setter, as well as add LLTV +/// tiers and liquidation cursors. /// @dev The fee setter can set the default and per-market settlement fee and continuous fee. /// @dev The fee claimer can claim the settlement fee and continuous fee. /// @dev When the claimer is set, the old claimer loses the unclaimed fees. @@ -203,7 +206,8 @@ contract Midnight is IMidnight { mapping(address loanToken => uint16[7]) public defaultSettlementFeeCbp; mapping(address loanToken => uint32) public defaultContinuousFee; mapping(address token => uint256) public claimableSettlementFee; - mapping(uint256 lltv => bool) public isLltvAllowed; + mapping(uint256 lltv => bool) public isLltvEnabled; + mapping(uint256 liquidationCursor => bool) public isLiquidationCursorEnabled; address public roleSetter; address public feeSetter; address public feeClaimer; @@ -256,15 +260,24 @@ contract Midnight is IMidnight { emit EventsLib.SetTickSpacingSetter(newTickSpacingSetter); } - /// @dev Allows a new LLTV tier. Tiers can only be added, never removed. + /// @dev Enables a new LLTV tier. Tiers can only be added, never removed. function addLltv(uint256 lltv) external { require(msg.sender == roleSetter, OnlyRoleSetter()); require(lltv <= WAD, InvalidLltv()); - isLltvAllowed[lltv] = true; + isLltvEnabled[lltv] = true; emit EventsLib.AddLltv(lltv); } - /// @dev Refines the tick spacing of a market. Can not increase (more ticks become accessible). + /// @dev Enables a liquidationCursor for use at market creation. Liquidation cursors can only be enabled, never + /// disabled. touchMarket checks the resulting maxLif for each market. + function addLiquidationCursor(uint256 liquidationCursor) external { + require(msg.sender == roleSetter, OnlyRoleSetter()); + require(liquidationCursor <= WAD, InvalidLiquidationCursor()); + isLiquidationCursorEnabled[liquidationCursor] = true; + emit EventsLib.AddLiquidationCursor(liquidationCursor); + } + + /// @dev Refines the tick spacing of a market. Cannot increase (more ticks become accessible). function setMarketTickSpacing(bytes32 id, uint256 newTickSpacing) external { require(msg.sender == tickSpacingSetter, OnlyTickSpacingSetter()); require(marketState[id].tickSpacing > 0, MarketNotCreated()); @@ -637,7 +650,8 @@ contract Midnight is IMidnight { uint256 _collateral = _position.collateral[i]; maxDebt += _collateral.mulDivDown(price, ORACLE_PRICE_SCALE).mulDivDown(_collateralParam.lltv, WAD); badDebt = badDebt.zeroFloorSub( - _collateral.mulDivUp(price, ORACLE_PRICE_SCALE).mulDivUp(WAD, _collateralParam.maxLif) + _collateral.mulDivUp(price, ORACLE_PRICE_SCALE) + .mulDivUp(WAD, maxLif(_collateralParam.lltv, _collateralParam.liquidationCursor)) ); _collateralBitmap = _collateralBitmap.clearBit(i); } @@ -666,7 +680,8 @@ contract Midnight is IMidnight { } if (repaidUnits > 0 || seizedAssets > 0) { - uint256 _maxLif = market.collateralParams[collateralIndex].maxLif; + uint256 lltv = market.collateralParams[collateralIndex].lltv; + uint256 _maxLif = maxLif(lltv, market.collateralParams[collateralIndex].liquidationCursor); uint256 lif = postMaturityMode ? UtilsLib.min(_maxLif, WAD + (_maxLif - WAD) * (block.timestamp - market.maturity) / TIME_TO_MAX_LIF) : _maxLif; @@ -678,10 +693,9 @@ contract Midnight is IMidnight { } if (!postMaturityMode) { - uint256 lltv = market.collateralParams[collateralIndex].lltv; // Note that debt >= maxDebt in this branch. - // The imprecision in this computation is at most a few hundred collateral or loan token assets. - uint256 maxRepaid = lltv < WAD + // lif * lltv >= WAD * WAD means LIF * LLTV >= 1, so the RCF is inactive (see LIQUIDATIONS). + uint256 maxRepaid = lif * lltv < WAD * WAD ? (_position.debt - maxDebt).mulDivUp(WAD * WAD, WAD * WAD - lif * lltv) : type(uint256).max; require( @@ -788,12 +802,10 @@ contract Midnight is IMidnight { address collateralToken = market.collateralParams[i].token; require(collateralToken > previousCollateralToken, CollateralParamsNotSorted()); uint256 lltv = market.collateralParams[i].lltv; - require(isLltvAllowed[lltv], LltvNotAllowed()); - require( - market.collateralParams[i].maxLif == maxLif(lltv, LIQUIDATION_CURSOR_LOW) - || market.collateralParams[i].maxLif == maxLif(lltv, LIQUIDATION_CURSOR_HIGH), - InvalidMaxLif() - ); + uint256 liquidationCursor = market.collateralParams[i].liquidationCursor; + require(isLltvEnabled[lltv], LltvNotEnabled()); + require(isLiquidationCursorEnabled[liquidationCursor], LiquidationCursorNotEnabled()); + require(maxLif(lltv, liquidationCursor) <= 2 * WAD, InvalidMaxLif()); previousCollateralToken = collateralToken; } diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 7d93054d..4fee909a 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -14,7 +14,7 @@ struct Market { struct CollateralParams { address token; uint256 lltv; - uint256 maxLif; + uint256 liquidationCursor; address oracle; } @@ -77,12 +77,14 @@ interface IMidnight { error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); error InvalidFeeIndex(); + error InvalidLiquidationCursor(); error InvalidLltv(); error InvalidMaxLif(); error InvalidOfferCaps(); error InvalidTickSpacing(); + error LiquidationCursorNotEnabled(); error LiquidatorGatedFromLiquidating(); - error LltvNotAllowed(); + error LltvNotEnabled(); error MakerCreditOrDebtIncreased(); error MarketLossFactorMaxedOut(); error MarketNotCreated(); @@ -128,7 +130,9 @@ interface IMidnight { function defaultSettlementFeeCbp(address loanToken, uint256 index) external view returns (uint16); function defaultContinuousFee(address loanToken) external view returns (uint32); function claimableSettlementFee(address token) external view returns (uint256); - function isLltvAllowed(uint256 lltv) external view returns (bool); + function isLltvEnabled(uint256 lltv) external view returns (bool); + function isLiquidationCursorEnabled(uint256 liquidationCursor) external view returns (bool); + function roleSetter() external view returns (address); function feeSetter() external view returns (address); function feeClaimer() external view returns (address); @@ -143,6 +147,7 @@ interface IMidnight { function setFeeClaimer(address newFeeClaimer) external; function setTickSpacingSetter(address newTickSpacingSetter) external; function addLltv(uint256 lltv) external; + function addLiquidationCursor(uint256 liquidationCursor) external; function setMarketTickSpacing(bytes32 id, uint256 newTickSpacing) external; function setMarketSettlementFee(bytes32 id, uint256 index, uint256 newSettlementFee) external; function setDefaultSettlementFee(address loanToken, uint256 index, uint256 newSettlementFee) external; diff --git a/src/libraries/ConstantsLib.sol b/src/libraries/ConstantsLib.sol index eb658036..33a80002 100644 --- a/src/libraries/ConstantsLib.sol +++ b/src/libraries/ConstantsLib.sol @@ -19,8 +19,6 @@ uint32 constant MAX_CONTINUOUS_FEE = uint32(uint256(0.01e18) / uint256(365 days) uint256 constant TIME_TO_MAX_LIF = 60 minutes; uint256 constant MAX_COLLATERALS = 128; uint256 constant MAX_COLLATERALS_PER_BORROWER = 16; -uint256 constant LIQUIDATION_CURSOR_LOW = 0.25e18; -uint256 constant LIQUIDATION_CURSOR_HIGH = 0.5e18; uint256 constant LIQUIDATION_LOCK_SLOT = uint256(keccak256("morpho.midnight.liquidationLocked")); bytes32 constant CALLBACK_SUCCESS = keccak256("morpho.midnight.callbackSuccess"); uint8 constant DEFAULT_TICK_SPACING = 4; @@ -30,8 +28,8 @@ function maxSettlementFee(uint256 index) pure returns (uint256) { return [MAX_SETTLEMENT_FEE_0_DAYS, MAX_SETTLEMENT_FEE_1_DAY, MAX_SETTLEMENT_FEE_7_DAYS, MAX_SETTLEMENT_FEE_30_DAYS, MAX_SETTLEMENT_FEE_90_DAYS, MAX_SETTLEMENT_FEE_180_DAYS, MAX_SETTLEMENT_FEE_360_DAYS][index]; } -/// @dev Returns the max LIF for the given lltv and cursor. -function maxLif(uint256 lltv, uint256 cursor) pure returns (uint256) { - return UtilsLib.mulDivDown(WAD, WAD, WAD - UtilsLib.mulDivDown(cursor, WAD - lltv, WAD)); +/// @dev Returns the max LIF for the given lltv and liquidationCursor. +function maxLif(uint256 lltv, uint256 liquidationCursor) pure returns (uint256) { + return UtilsLib.mulDivDown(WAD, WAD, WAD - UtilsLib.mulDivDown(liquidationCursor, WAD - lltv, WAD)); } // forgefmt: disable-end diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 6fb83527..bc99e229 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -12,6 +12,7 @@ library EventsLib { event SetFeeSetter(address indexed feeSetter); event SetTickSpacingSetter(address indexed tickSpacingSetter); event AddLltv(uint256 lltv); + event AddLiquidationCursor(uint256 liquidationCursor); event SetMarketTickSpacing(bytes32 indexed id_, uint256 newTickSpacing); event SetMarketSettlementFee(bytes32 indexed id_, uint256 indexed index, uint256 newSettlementFee); event SetDefaultSettlementFee(address indexed loanToken, uint256 indexed index, uint256 newSettlementFee); diff --git a/src/ratifiers/libraries/HashLib.sol b/src/ratifiers/libraries/HashLib.sol index 13d8be01..f3afe576 100644 --- a/src/ratifiers/libraries/HashLib.sol +++ b/src/ratifiers/libraries/HashLib.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.0; 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("CollateralParams(address token,uint256 lltv,uint256 liquidationCursor,address oracle)"). +bytes32 constant COLLATERAL_PARAMS_TYPEHASH = 0x39ed3f928d24fd00574b1a02aba9c2483abcf5d9a3a366118c9a5aa29885b841; /// @dev keccak256(bytes.concat(MARKET_TYPE, COLLATERAL_PARAMS_TYPE)). -bytes32 constant MARKET_TYPEHASH = 0x358117e98511cc3df97175dca58053b06675b43ad090b0553f8a1eff008b6e2e; +bytes32 constant MARKET_TYPEHASH = 0xc7217937e3e4792f008803233f9ab5733ae21550dfd9a82f7a931202d8519182; /// @dev keccak256(bytes.concat(OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). -bytes32 constant OFFER_TYPEHASH = 0x6bd2a06ec6952feb97c3e3b4f7de6c342f12b1ac769d5c91368271af636c85b7; +bytes32 constant OFFER_TYPEHASH = 0xbe02458b8446cb4d08b43541761ff65d260a0247247652479c648e920469de95; 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 0x3ee1994da955d5aa3e32de9e53261da443a2406198e3fc5d504c7e8b2a947953; + if (height == 1) return 0xc658b2e55d2e8a0383982564366d7008a7a0743863c09905382e17de18409edb; + if (height == 2) return 0x4e0adcc9afd3d2080aac34120ed811e21a333779a62906005c2379e3f69eead2; + if (height == 3) return 0x7dc62fe405e0f2bb86f8aa4917949a80f9a43b0533439da84befc5f86db9f8b8; + if (height == 4) return 0x827956f8aafdb5e1977c15463f7acf66574e20f7816c609330a1c384d5d8f676; + if (height == 5) return 0xebb0c1d27456093ca3da34dda5806074909dd149beee083cc5773693bd2173e2; + if (height == 6) return 0x7762fbf5c7a7ace0fba4c779124cb4d3eb2e36dfca7b69db8c58e7cc338f06f3; + if (height == 7) return 0xee68d4c6dbc5ebd3f2a2b888e1e8183e9c3e2abb5f6b11ae29c7f9e4e2f1bd08; + if (height == 8) return 0x51a23c87d840ec3998fa1ca65c8252b77695e334d86da192578ac27e10904963; + if (height == 9) return 0x0118309844ce8ed678b69710edade8185443a49bb39b2fe3be46a0df1c73c41a; + return 0x95ee7bf616c256f973563b7bcd7ac6711b569fe4dd8c059518e1c61ea542bf63; } 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 0x0a09bd2194e87e661b564b9a3a60f18a3d1558f63c51ea3b7668d69b52b34c85; + if (height == 12) return 0x7658b4c053abc5076abe87bde4846e9c74001d8173009ec166fe69fab803605b; + if (height == 13) return 0x60fa6620ed4ae28578399b4eb8900145883babafaf5e0b7b88f105b5873cbf22; + if (height == 14) return 0x2ff0610678245028de334e1bf7de8191b7c2d2cadd004a545134f2e585461dbf; + if (height == 15) return 0xa45443482c81a668a9119c2659c87fa22f66c89528237a86b8c8c6997f85f81f; + if (height == 16) return 0x400c36ea65fc9f5d0d018fbc2877a8eed7ba4ed735d2b0f791621a8652598043; + if (height == 17) return 0x23cbda45134146dc786abd8f7cf5efab3a10f0419d7087a35211f62082c64be7; + if (height == 18) return 0x3be9250a943a7ae0c1ce9e807b89fa4d49752a9f5c4cc195e7c9560e49072e0e; + if (height == 19) return 0xb50a451161608cb6216239dcccb61dd9b3da0c7aa2e607f69c8295aec6b859e2; + if (height == 20) return 0x25c08c52eda13f831fa57fcd45bb0d89b84222c44e77e658fb58687a6cd6ebd5; revert TreeTooHigh(); } } @@ -78,7 +78,7 @@ library HashLib { COLLATERAL_PARAMS_TYPEHASH, collateralParams.token, collateralParams.lltv, - collateralParams.maxLif, + collateralParams.liquidationCursor, collateralParams.oracle ) ); diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index f901bdab..96954310 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {IMidnight, Market, CollateralParams, Offer} from "../src/interfaces/IMidnight.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {ERC20} from "./erc20s/ERC20.sol"; import {MAX_TICK} from "../src/libraries/TickLib.sol"; @@ -24,8 +24,8 @@ contract AuthorizationTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); diff --git a/test/BaseTest.sol b/test/BaseTest.sol index a662137e..0621e6a5 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -20,7 +20,6 @@ import { WAD, ORACLE_PRICE_SCALE, MAX_COLLATERALS, - LIQUIDATION_CURSOR_LOW, maxSettlementFee as _maxSettlementFee, maxLif as _maxLif } from "../src/libraries/ConstantsLib.sol"; @@ -30,16 +29,11 @@ import {EcrecoverRatifier} from "../src/ratifiers/EcrecoverRatifier.sol"; import {EcrecoverAuthorizer} from "../src/periphery/EcrecoverAuthorizer.sol"; uint256 constant MAX_TEST_AMOUNT = type(uint128).max; -/// @dev The LLTV tiers registered in tests, copied from Morpho Blue's enabled tiers (excluding zero, including WAD). -uint256 constant LLTV_0 = 0.385e18; -uint256 constant LLTV_1 = 0.625e18; -uint256 constant LLTV_2 = 0.77e18; -uint256 constant LLTV_3 = 0.86e18; -uint256 constant LLTV_4 = 0.915e18; -uint256 constant LLTV_5 = 0.945e18; -uint256 constant LLTV_6 = 0.965e18; -uint256 constant LLTV_7 = 0.98e18; -uint256 constant LLTV_8 = 1e18; +/// @dev The default LLTV enabled in tests. +uint256 constant LLTV = 0.77e18; + +/// @dev The default liquidationCursor enabled in tests. +uint256 constant LIQUIDATION_CURSOR = 0.3e18; abstract contract BaseTest is Test { using UtilsLib for uint256; @@ -72,10 +66,10 @@ abstract contract BaseTest is Test { midnight.setFeeSetter(address(this)); midnight.setTickSpacingSetter(address(this)); - uint256[9] memory tiers = [LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7, LLTV_8]; - for (uint256 i = 0; i < tiers.length; i++) { - midnight.addLltv(tiers[i]); - } + // Enable the default liquidationCursor at deployment time. + midnight.addLiquidationCursor(LIQUIDATION_CURSOR); + + midnight.addLltv(LLTV); uint256 _privateKey; (borrower, _privateKey) = makeAddrAndKey("borrower"); @@ -277,12 +271,6 @@ abstract contract BaseTest is Test { return arr; } - /// @dev Returns an allowed LLTV tier based on a seed value. - function allowedLltv(uint256 seed) internal pure returns (uint256) { - uint256[9] memory tiers = [LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7, LLTV_8]; - return tiers[seed % 9]; - } - /// @dev Returns a market with sorted, unique collateralParams, valid lltv/maxLif, and a creatable TTM. function validMarket(Market memory market) internal view returns (Market memory) { uint256 len = @@ -292,9 +280,8 @@ abstract contract BaseTest is Test { for (uint256 i = 0; i < len; i++) { collateralParams[i].token = address(uint160(uint256(keccak256(abi.encode(market.collateralParams[i].token, i))))); - uint256 lltv = allowedLltv(market.collateralParams[i].lltv); - collateralParams[i].lltv = lltv; - collateralParams[i].maxLif = maxLif(lltv, LIQUIDATION_CURSOR_LOW); + collateralParams[i].lltv = LLTV; + collateralParams[i].liquidationCursor = LIQUIDATION_CURSOR; } collateralParams = sortCollateralParams(collateralParams); market.collateralParams = collateralParams; @@ -344,8 +331,12 @@ abstract contract BaseTest is Test { return a > b ? a - b : b - a; } - function maxLif(uint256 lltv, uint256 cursor) internal pure returns (uint256) { - return _maxLif(lltv, cursor); + function maxLif(uint256 lltv, uint256 liquidationCursor) internal pure returns (uint256) { + return _maxLif(lltv, liquidationCursor); + } + + function maxLif(CollateralParams memory params) internal pure returns (uint256) { + return _maxLif(params.lltv, params.liquidationCursor); } function maxSettlementFee(uint256 index) internal pure returns (uint256) { diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index 3507e525..f6d36b22 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -7,7 +7,7 @@ import {EventsLib} from "../src/libraries/EventsLib.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; import {IMidnight, Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; -import {BaseTest, MAX_TEST_AMOUNT} from "./BaseTest.sol"; +import {BaseTest, LLTV, MAX_TEST_AMOUNT, LIQUIDATION_CURSOR} from "./BaseTest.sol"; uint256 constant MAX_CREDIT = MAX_TEST_AMOUNT / 4; @@ -28,8 +28,8 @@ contract ContinuousFeeTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); diff --git a/test/EcrecoverRatifierIntegrationTest.sol b/test/EcrecoverRatifierIntegrationTest.sol index 0c1776d7..9bf8d84c 100644 --- a/test/EcrecoverRatifierIntegrationTest.sol +++ b/test/EcrecoverRatifierIntegrationTest.sol @@ -8,7 +8,7 @@ import {WAD} from "../src/libraries/ConstantsLib.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; /// @dev Tests covering the merkle/signature flow of `EcrecoverRatifier` end-to-end via `Midnight.take`. /// `EcrecoverRatifierTest` covers the ratifier in isolation; this file pins the integration with Midnight. @@ -30,8 +30,8 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -39,8 +39,8 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/FrontendSignatureTest.sol b/test/FrontendSignatureTest.sol index df675cbe..8369b38e 100644 --- a/test/FrontendSignatureTest.sol +++ b/test/FrontendSignatureTest.sol @@ -8,11 +8,12 @@ import {Signature} from "../src/ratifiers/interfaces/IEcrecoverRatifier.sol"; import {CALLBACK_SUCCESS} from "../src/libraries/ConstantsLib.sol"; import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; -// Paste from frontend output. -address constant ACCOUNT = 0xFDa6883171208B36122229505FB2D6F30c052311; -uint8 constant SIG_V = 28; -bytes32 constant SIG_R = 0xb7a8c34b3aa87d799f2bd6e01a36c6a48673313015c592fc4137043b37ee80c6; -bytes32 constant SIG_S = 0x5161ce684b17e81a0b297441856e8d8b498c541116fd6dde5d87e5624b847afb; +// Paste from frontend output (sign-root.ts), regenerated after the CollateralParams.maxLif -> liquidationCursor type +// change and continuousFeeCap addition. +address constant ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; +uint8 constant SIG_V = 27; +bytes32 constant SIG_R = 0x44bce959b3a15de43189cdf1d973e26a63f6046a38e25582a1200b09b843045d; +bytes32 constant SIG_S = 0x0d6d5b67b21ed763d9dbf0632e727cccc54d33513972eb0f78473e5717d3514d; address constant RATIFIER = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; diff --git a/test/GateTest.sol b/test/GateTest.sol index f587e812..d0c4680a 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.0; import {IMidnight, Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; import {IEnterGate, ILiquidatorGate} from "../src/interfaces/IGate.sol"; -import {LIQUIDATION_CURSOR_LOW, ORACLE_PRICE_SCALE} from "../src/libraries/ConstantsLib.sol"; +import {ORACLE_PRICE_SCALE} from "../src/libraries/ConstantsLib.sol"; import {MAX_TICK} from "../src/libraries/TickLib.sol"; -import {BaseTest, MAX_TEST_AMOUNT} from "./BaseTest.sol"; +import {BaseTest, LLTV, MAX_TEST_AMOUNT, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {Oracle} from "./helpers/Oracle.sol"; contract WhitelistGate is IEnterGate, ILiquidatorGate { @@ -48,9 +48,9 @@ contract GateTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, + lltv: LLTV, oracle: address(oracle1), - maxLif: maxLif(0.77e18, LIQUIDATION_CURSOR_LOW) + liquidationCursor: LIQUIDATION_CURSOR }) ); market.collateralParams = sortCollateralParams(market.collateralParams); @@ -61,9 +61,9 @@ contract GateTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, + lltv: LLTV, oracle: address(oracle1), - maxLif: maxLif(0.77e18, LIQUIDATION_CURSOR_LOW) + liquidationCursor: LIQUIDATION_CURSOR }) ); gatedMarket.collateralParams = sortCollateralParams(gatedMarket.collateralParams); diff --git a/test/HashLibTest.sol b/test/HashLibTest.sol index 42f5b5e9..64bf9994 100644 --- a/test/HashLibTest.sol +++ b/test/HashLibTest.sol @@ -10,7 +10,8 @@ import { } from "../src/ratifiers/libraries/HashLib.sol"; import {Market} from "../src/interfaces/IMidnight.sol"; -bytes constant COLLATERAL_PARAMS_TYPE = "CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"; +bytes constant COLLATERAL_PARAMS_TYPE = + "CollateralParams(address token,uint256 lltv,uint256 liquidationCursor,address oracle)"; bytes constant MARKET_TYPE = "Market(address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; bytes constant OFFER_TYPE = diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index 01d59ad7..e6c73c75 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -25,7 +25,7 @@ contract IdLibTest is Test { if (market1.collateralParams[i].lltv != market2.collateralParams[i].lltv) { sameCollaterals = false; } - if (market1.collateralParams[i].maxLif != market2.collateralParams[i].maxLif) { + if (market1.collateralParams[i].liquidationCursor != market2.collateralParams[i].liquidationCursor) { sameCollaterals = false; } if (market1.collateralParams[i].oracle != market2.collateralParams[i].oracle) { diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 330dc54e..e55d1cf0 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -7,7 +7,6 @@ import { ORACLE_PRICE_SCALE, TIME_TO_MAX_LIF, MAX_CONTINUOUS_FEE, - LIQUIDATION_CURSOR_LOW, CALLBACK_SUCCESS } from "../src/libraries/ConstantsLib.sol"; import {IMidnight, Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; @@ -15,7 +14,7 @@ import {IdLib} from "../src/libraries/IdLib.sol"; import {IOracle} from "../src/interfaces/IOracle.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {Oracle} from "./helpers/Oracle.sol"; -import {BaseTest, MAX_TEST_AMOUNT, LLTV_8} from "./BaseTest.sol"; +import {BaseTest, MAX_TEST_AMOUNT, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {stdError} from "../lib/forge-std/src/StdError.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; @@ -50,8 +49,8 @@ contract LiquidationTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -59,8 +58,8 @@ contract LiquidationTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.86e18, - maxLif: maxLif(0.86e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); @@ -209,7 +208,7 @@ contract LiquidationTest is BaseTest { assertEq(repaidUnits, repaid, "repaid units"); assertEq( seizedAssets, - repaid.mulDivDown(market.collateralParams[0].maxLif, WAD) + repaid.mulDivDown(maxLif(market.collateralParams[0]), WAD) .mulDivDown(ORACLE_PRICE_SCALE, liquidationOraclePrice), "seized assets" ); @@ -228,7 +227,7 @@ contract LiquidationTest is BaseTest { seized, 0, UtilsLib.min( - units.mulDivDown(market.collateralParams[0].maxLif, WAD) + units.mulDivDown(maxLif(market.collateralParams[0]), WAD) .mulDivDown(ORACLE_PRICE_SCALE, liquidationOraclePrice), initialCollateral ) @@ -242,7 +241,7 @@ contract LiquidationTest is BaseTest { assertEq( repaidUnits, seized.mulDivUp(liquidationOraclePrice, ORACLE_PRICE_SCALE) - .mulDivUp(WAD, market.collateralParams[0].maxLif), + .mulDivUp(WAD, maxLif(market.collateralParams[0])), "repaid units" ); assertEq(seizedAssets, seized, "seized assets"); @@ -272,9 +271,9 @@ contract LiquidationTest is BaseTest { uint256 expectedBadDebt = _badDebt(); uint256 maxRepaid = midnight.collateral(id, borrower, collateralIndex) .mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE) - .mulDivDown(WAD, market.collateralParams[collateralIndex].maxLif); + .mulDivDown(WAD, maxLif(market.collateralParams[collateralIndex])); repaid = bound(repaid, 0, UtilsLib.min(units - expectedBadDebt, maxRepaid)); - uint256 expectedSeizedAssets = repaid.mulDivDown(market.collateralParams[collateralIndex].maxLif, WAD) + uint256 expectedSeizedAssets = repaid.mulDivDown(maxLif(market.collateralParams[collateralIndex]), WAD) .mulDivDown(ORACLE_PRICE_SCALE, liquidationOraclePrice); vm.prank(caller); @@ -298,7 +297,7 @@ contract LiquidationTest is BaseTest { setupMarket(market, units); vm.warp(market.maturity + TIME_TO_MAX_LIF); // Warp to post-maturity for full LIF. - uint256 _maxLif = market.collateralParams[0].maxLif; + uint256 _maxLif = maxLif(market.collateralParams[0]); uint256 collateral = midnight.collateral(id, borrower, 0); // Price must be high enough that seized assets for (units + 1) don't exceed available collateral. @@ -450,7 +449,7 @@ contract LiquidationTest is BaseTest { Oracle(market.collateralParams[0].oracle).setPrice(liquidationOraclePrice); uint256 debtAfterBadDebt = units - _badDebt(); uint256 maxRepaid = _maxRepaid(units, debtAfterBadDebt, liquidationOraclePrice); - uint256 lif0 = market.collateralParams[0].maxLif; + uint256 lif0 = maxLif(market.collateralParams[0]); uint256 maxRepaidFromCollat = midnight.collateral(id, borrower, 0) .mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE).mulDivDown(WAD, lif0); repaid = bound(repaid, 0, UtilsLib.min(UtilsLib.min(maxRepaid, debtAfterBadDebt), maxRepaidFromCollat)); @@ -511,7 +510,7 @@ contract LiquidationTest is BaseTest { assertEq( midnight.collateral(id, borrower, 0), initialCollateral - - repaid.mulDivDown(market.collateralParams[0].maxLif, WAD) + - repaid.mulDivDown(maxLif(market.collateralParams[0]), WAD) .mulDivDown(ORACLE_PRICE_SCALE, liquidationOraclePrice), "collateral" ); @@ -536,7 +535,7 @@ contract LiquidationTest is BaseTest { midnight.liquidate(market, 0, 0, repaid, borrower, true, address(this), address(0), ""); - uint256 lif = WAD + (market.collateralParams[0].maxLif - WAD) * delay / TIME_TO_MAX_LIF; + uint256 lif = WAD + (maxLif(market.collateralParams[0]) - WAD) * delay / TIME_TO_MAX_LIF; assertEq(midnight.debt(id, borrower), units - repaid, "debt"); assertEq( @@ -611,7 +610,7 @@ contract LiquidationTest is BaseTest { uint256 lltv = market.collateralParams[0].lltv; uint256 collatAmount = units.mulDivUp(WAD, lltv); uint256 maxRepaid = _maxRepaid(units, units, liquidationOraclePrice); - uint256 lif0 = market.collateralParams[0].maxLif; + uint256 lif0 = maxLif(market.collateralParams[0]); uint256 remainingRepayable = collatAmount.mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE) .mulDivDown(WAD, lif0).zeroFloorSub(maxRepaid); market.rcfThreshold = bound(rcfThreshold, remainingRepayable + 1, type(uint256).max); @@ -640,7 +639,7 @@ contract LiquidationTest is BaseTest { uint256 maxRepaid = _maxRepaid(units, units, liquidationOraclePrice); vm.assume(maxRepaid < units); // needed because of the round up. uint256 remainingRepayable = collatAmount.mulDivDown(liquidationOraclePrice, ORACLE_PRICE_SCALE) - .mulDivDown(WAD, market.collateralParams[0].maxLif).zeroFloorSub(maxRepaid); + .mulDivDown(WAD, maxLif(market.collateralParams[0])).zeroFloorSub(maxRepaid); market.rcfThreshold = bound(rcfThreshold, 0, remainingRepayable); collateralize(market, borrower, units); @@ -694,8 +693,8 @@ contract LiquidationTest is BaseTest { // Price is 1 initially, assume liquidatable but no bad debt. uint256 maxDebt = collateral1.mulDivDown(market.collateralParams[0].lltv, WAD) + collateral2.mulDivDown(market.collateralParams[1].lltv, WAD); - uint256 repayableDebt = collateral1.mulDivDown(WAD, market.collateralParams[0].maxLif) - + collateral2.mulDivDown(WAD, market.collateralParams[1].maxLif); + uint256 repayableDebt = collateral1.mulDivDown(WAD, maxLif(market.collateralParams[0])) + + collateral2.mulDivDown(WAD, maxLif(market.collateralParams[1])); units = bound(units, maxDebt, repayableDebt); vm.assume(units > maxDebt); @@ -726,7 +725,7 @@ contract LiquidationTest is BaseTest { // If it had bad debt, this can be taken into account separately. assertEq(_badDebt(), 0, "no bad debt"); - uint256 collateralNeededToRepayAll = units.mulDivDown(market.collateralParams[0].maxLif, WAD); + uint256 collateralNeededToRepayAll = units.mulDivDown(maxLif(market.collateralParams[0]), WAD); if (collateralNeededToRepayAll <= collateral1) { midnight.liquidate(market, 0, 0, units, borrower, false, address(this), address(0), ""); } else { @@ -775,7 +774,7 @@ contract LiquidationTest is BaseTest { + otherCollat.mulDivDown(market.collateralParams[otherIdx].lltv, WAD); uint256 maxR = (units - _maxDebt) - .mulDivUp(WAD * WAD, WAD * WAD - market.collateralParams[liqIdx].maxLif * market.collateralParams[liqIdx].lltv); + .mulDivUp(WAD * WAD, WAD * WAD - maxLif(market.collateralParams[liqIdx]) * market.collateralParams[liqIdx].lltv); midnight.liquidate(market, liqIdx, 0, maxR, borrower, false, address(this), address(0), ""); } @@ -923,7 +922,7 @@ contract LiquidationTest is BaseTest { uint256 price = IOracle(_collateral.oracle).price(); badDebt = badDebt.zeroFloorSub( midnight.collateral(id, borrower, i).mulDivUp(price, ORACLE_PRICE_SCALE) - .mulDivUp(WAD, _collateral.maxLif) + .mulDivUp(WAD, maxLif(_collateral)) ); require(i < 128, "i is too large"); // forge-lint: disable-next-line(unsafe-typecast) as i < 128 is checked above. @@ -935,24 +934,24 @@ contract LiquidationTest is BaseTest { /// @dev A price below which the position will create bad debt. function badDebtPriceDown(uint256 units) internal view returns (uint256) { uint256 lltv = market.collateralParams[0].lltv; - uint256 maxLif = market.collateralParams[0].maxLif; + uint256 _maxLif = maxLif(market.collateralParams[0]); uint256 collateral = units.mulDivUp(WAD, lltv); - return (units - 1).mulDivDown(maxLif, WAD).mulDivDown(ORACLE_PRICE_SCALE, collateral); + return (units - 1).mulDivDown(_maxLif, WAD).mulDivDown(ORACLE_PRICE_SCALE, collateral); } /// @dev A price above which full repayment does not exceed available collateral. function fullRepaymentPrice(uint256 units) internal view returns (uint256) { uint256 lltv = market.collateralParams[0].lltv; - uint256 maxLif = market.collateralParams[0].maxLif; + uint256 _maxLif = maxLif(market.collateralParams[0]); uint256 collateral = units.mulDivUp(WAD, lltv); - return units.mulDivUp(maxLif, WAD).mulDivUp(ORACLE_PRICE_SCALE, collateral); + return units.mulDivUp(_maxLif, WAD).mulDivUp(ORACLE_PRICE_SCALE, collateral); } function _maxRepaid(uint256 units, uint256 debt, uint256 oraclePrice) internal view returns (uint256) { uint256 lltv = market.collateralParams[0].lltv; uint256 collatAmount = units.mulDivUp(WAD, lltv); uint256 _maxDebt = collatAmount.mulDivDown(oraclePrice, ORACLE_PRICE_SCALE).mulDivDown(lltv, WAD); - return (debt - _maxDebt).mulDivUp(WAD * WAD, WAD * WAD - market.collateralParams[0].maxLif * lltv); + return (debt - _maxDebt).mulDivUp(WAD * WAD, WAD * WAD - maxLif(market.collateralParams[0]) * lltv); } function _setupUnhealthy(uint256 units, uint256 liquidationOraclePrice) @@ -972,14 +971,15 @@ contract LiquidationTest is BaseTest { function testLiquidatePreMaturityLltvWad(uint256 units) public { units = bound(units, 2, MAX_UNITS); - // Override market to use LLTV_8 = WAD on collateral 0. + // Override market to use LLTV = WAD on collateral 0. + midnight.addLltv(WAD); delete market.collateralParams; market.collateralParams .push( CollateralParams({ token: address(collateralToken1), - lltv: LLTV_8, - maxLif: maxLif(LLTV_8, LIQUIDATION_CURSOR_LOW), + lltv: WAD, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index fe1cefae..a0199367 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -6,7 +6,7 @@ import {Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; import {ORACLE_PRICE_SCALE} from "../src/libraries/ConstantsLib.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {MAX_TICK} from "../src/libraries/TickLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; uint256 constant MAX_AMOUNT = type(uint128).max; @@ -25,8 +25,8 @@ contract MaxAmountsTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index 47f2a844..faeddc13 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -19,7 +19,7 @@ import { PermitKind } from "../src/periphery/interfaces/IMidnightBundles.sol"; import {Permit2 as VendorPermit2} from "./vendor/Permit2.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; contract MidnightBundlesTest is BaseTest { using UtilsLib for uint256; @@ -51,8 +51,8 @@ contract MidnightBundlesTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -60,8 +60,8 @@ contract MidnightBundlesTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 08827607..adb19a8e 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -17,7 +17,7 @@ import {EventsLib} from "../src/libraries/EventsLib.sol"; import {ERC20} from "./erc20s/ERC20.sol"; import {Oracle} from "./helpers/Oracle.sol"; import {RevertingOracle} from "./helpers/RevertingOracle.sol"; -import {BaseTest, MAX_TEST_AMOUNT} from "./BaseTest.sol"; +import {BaseTest, LLTV, MAX_TEST_AMOUNT, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import { MAX_COLLATERALS, MAX_COLLATERALS_PER_BORROWER, @@ -48,8 +48,8 @@ contract OtherFunctionsTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -57,8 +57,8 @@ contract OtherFunctionsTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); @@ -316,7 +316,11 @@ contract OtherFunctionsTest is BaseTest { for (uint256 i = 0; i < marketFromId.collateralParams.length; i++) { assertEq(_market.collateralParams[i].token, marketFromId.collateralParams[i].token, "collateral token"); assertEq(_market.collateralParams[i].lltv, marketFromId.collateralParams[i].lltv, "lltv"); - assertEq(_market.collateralParams[i].maxLif, marketFromId.collateralParams[i].maxLif, "maxLif"); + assertEq( + _market.collateralParams[i].liquidationCursor, + marketFromId.collateralParams[i].liquidationCursor, + "liquidationCursor" + ); assertEq(_market.collateralParams[i].oracle, marketFromId.collateralParams[i].oracle, "oracle"); } } @@ -372,8 +376,8 @@ contract OtherFunctionsTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(revertingOracle) }); @@ -396,8 +400,8 @@ contract OtherFunctionsTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(revertingOracle) }); @@ -426,7 +430,7 @@ contract OtherFunctionsTest is BaseTest { ERC20 token = new ERC20("", ""); Oracle _oracle = new Oracle(); collateralParams[i] = CollateralParams({ - token: address(token), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(_oracle) + token: address(token), lltv: LLTV, liquidationCursor: LIQUIDATION_CURSOR, oracle: address(_oracle) }); } collateralParams = sortCollateralParams(collateralParams); @@ -483,42 +487,48 @@ contract OtherFunctionsTest is BaseTest { _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](2); collateralParams[0] = CollateralParams({ - token: address(uint160(2)), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(uint160(2)), lltv: LLTV, liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }); collateralParams[1] = CollateralParams({ - token: address(uint160(1)), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle2) + token: address(uint160(1)), lltv: LLTV, liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }); _market.collateralParams = collateralParams; vm.expectRevert(IMidnight.CollateralParamsNotSorted.selector); midnight.touchMarket(_market); } - function testLltvNotAllowedAboveWad(uint256 lltv) public { + function testLltvNotEnabledAboveWad(uint256 lltv) public { lltv = bound(lltv, WAD + 1, type(uint256).max); Market memory _market; _market.loanToken = address(loanToken); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: lltv, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: lltv, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); _market.collateralParams = collateralParams; - vm.expectRevert(IMidnight.LltvNotAllowed.selector); + vm.expectRevert(IMidnight.LltvNotEnabled.selector); midnight.touchMarket(_market); } - function testLltvNotAllowedBelowWad() public { - // 0.5e18 is not an allowed LLTV tier + function testLltvNotEnabledBelowWad() public { + // 0.5e18 is not an enabled LLTV tier uint256 lltv = 0.5e18; Market memory _market; _market.loanToken = address(loanToken); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: lltv, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: lltv, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); _market.collateralParams = collateralParams; - vm.expectRevert(IMidnight.LltvNotAllowed.selector); + vm.expectRevert(IMidnight.LltvNotEnabled.selector); midnight.touchMarket(_market); } @@ -638,54 +648,41 @@ contract OtherFunctionsTest is BaseTest { assertEq(collateralBitmap & (1 << collateralIndex), 0, "liquidated collateral bit should be cleared"); } - // LIF validation tests. - - function testInvalidLif(uint256 lif) public { - lif = bound(lif, 0, type(uint256).max); - uint256 lltv = 0.77e18; - vm.assume(lif != maxLif(lltv, 0.25e18)); - vm.assume(lif != maxLif(lltv, 0.5e18)); - - Market memory _market; - _market.loanToken = address(loanToken); - _market.maturity = vm.getBlockTimestamp() + 100; - CollateralParams[] memory collateralParams = new CollateralParams[](1); - collateralParams[0] = - CollateralParams({token: address(collateralToken1), lltv: lltv, maxLif: lif, oracle: address(oracle1)}); - _market.collateralParams = collateralParams; + // LiquidationCursor validation tests. - vm.expectRevert(IMidnight.InvalidMaxLif.selector); - midnight.touchMarket(_market); - } + function testLiquidationCursorNotEnabled(uint256 liquidationCursor) public { + vm.assume(liquidationCursor != LIQUIDATION_CURSOR); + uint256 lltv = LLTV; - function testValidLifCursor025() public { - uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: lltv, maxLif: maxLif(lltv, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), lltv: lltv, liquidationCursor: liquidationCursor, oracle: address(oracle1) }); _market.collateralParams = collateralParams; + vm.expectRevert(IMidnight.LiquidationCursorNotEnabled.selector); midnight.touchMarket(_market); - assertEq(midnight.tickSpacing(toId(_market)) > 0, true, "market created with cursor 0.25"); } - function testValidLifCursor05() public { - uint256 lltv = 0.77e18; + function testValidLifLiquidationCursor() public { + uint256 lltv = LLTV; Market memory _market; _market.loanToken = address(loanToken); - _market.maturity = vm.getBlockTimestamp() + 200; + _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: lltv, maxLif: maxLif(lltv, 0.5e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: lltv, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); _market.collateralParams = collateralParams; midnight.touchMarket(_market); - assertEq(midnight.tickSpacing(toId(_market)) > 0, true, "market created with cursor 0.5"); + assertEq(midnight.tickSpacing(toId(_market)) > 0, true, "market created with enabled liquidationCursor"); } function testMarketStateGetter(Market memory _market, uint256 _defaultContinuousFee) public { diff --git a/test/SetterRatifierTest.sol b/test/SetterRatifierTest.sol index b374aef8..da942216 100644 --- a/test/SetterRatifierTest.sol +++ b/test/SetterRatifierTest.sol @@ -8,7 +8,7 @@ import {ISetterRatifier} from "../src/ratifiers/interfaces/ISetterRatifier.sol"; import {CALLBACK_SUCCESS} from "../src/libraries/ConstantsLib.sol"; import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; import {MAX_TICK} from "../src/libraries/TickLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; contract SetterRatifierTest is BaseTest { SetterRatifier internal setterRatifier; @@ -24,7 +24,10 @@ contract SetterRatifierTest is BaseTest { market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams = new CollateralParams[](1); market.collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); offer.market = market; diff --git a/test/SettersTest.sol b/test/SettersTest.sol index bcd8ac21..a726882d 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -13,7 +13,7 @@ import { MAX_SETTLEMENT_FEE_180_DAYS, MAX_SETTLEMENT_FEE_360_DAYS } from "../src/libraries/ConstantsLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {IMidnight, Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; import {Midnight} from "../src/Midnight.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; @@ -80,13 +80,13 @@ contract SettersTest is BaseTest { function testAddLltvSuccess(uint256 lltv) public { lltv = bound(lltv, 0, WAD); - vm.assume(!midnight.isLltvAllowed(lltv)); + vm.assume(!midnight.isLltvEnabled(lltv)); vm.expectEmit(); emit EventsLib.AddLltv(lltv); midnight.addLltv(lltv); - assertTrue(midnight.isLltvAllowed(lltv)); + assertTrue(midnight.isLltvEnabled(lltv)); } function testAddLltvOnlyRoleSetter(address rdm, uint256 lltv) public { @@ -122,7 +122,10 @@ contract SettersTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: loanToken, @@ -240,6 +243,83 @@ contract SettersTest is BaseTest { midnight.setFeeClaimer(makeAddr("newRecipient")); } + // LiquidationCursor tests + + function testAddLiquidationCursorSuccess(uint256 liquidationCursor) public { + liquidationCursor = bound(liquidationCursor, 0, WAD); + // Use a cursor that isn't enabled at deployment, so market creation is initially rejected. + vm.assume(liquidationCursor != LIQUIDATION_CURSOR); + + CollateralParams[] memory collateralParams = new CollateralParams[](1); + collateralParams[0] = CollateralParams({ + token: address(collateralToken1), lltv: LLTV, liquidationCursor: liquidationCursor, oracle: address(oracle1) + }); + Market memory market = Market({ + loanToken: address(loanToken), + maturity: vm.getBlockTimestamp() + 1 days, + collateralParams: collateralParams, + rcfThreshold: 0, + enterGate: address(0), + liquidatorGate: address(0) + }); + + // A market using a not-yet-enabled liquidationCursor is rejected. + vm.expectRevert(IMidnight.LiquidationCursorNotEnabled.selector); + midnight.touchMarket(market); + + // Enabling it emits the event and flips the mapping. + vm.expectEmit(); + emit EventsLib.AddLiquidationCursor(liquidationCursor); + midnight.addLiquidationCursor(liquidationCursor); + assertTrue(midnight.isLiquidationCursorEnabled(liquidationCursor), "liquidationCursor enabled"); + + // The same market can now be created. + midnight.touchMarket(market); + assertTrue(midnight.tickSpacing(toId(market)) > 0, "market created with added liquidationCursor"); + } + + function testAddLiquidationCursorOnlyRoleSetter(address rdm, uint256 liquidationCursor) public { + vm.assume(rdm != address(this)); + liquidationCursor = bound(liquidationCursor, 0, WAD); + vm.prank(rdm); + vm.expectRevert(IMidnight.OnlyRoleSetter.selector); + midnight.addLiquidationCursor(liquidationCursor); + } + + function testAddLiquidationCursorAboveOneReverts(uint256 liquidationCursor) public { + liquidationCursor = bound(liquidationCursor, WAD + 1, type(uint256).max); + vm.expectRevert(IMidnight.InvalidLiquidationCursor.selector); + midnight.addLiquidationCursor(liquidationCursor); + } + + function testTouchMarketRejectsMaxLifAboveTwoWad() public { + uint256 liquidationCursor = 0.814e18; + midnight.addLiquidationCursor(liquidationCursor); + + // A low LLTV combined with this liquidationCursor yields a maxLif above 2 WAD. + uint256 lowLltv = 0.385e18; + midnight.addLltv(lowLltv); + + CollateralParams[] memory collateralParams = new CollateralParams[](1); + collateralParams[0] = CollateralParams({ + token: address(collateralToken1), + lltv: lowLltv, + liquidationCursor: liquidationCursor, + oracle: address(oracle1) + }); + Market memory market = Market({ + loanToken: address(loanToken), + maturity: vm.getBlockTimestamp() + 1 days, + collateralParams: collateralParams, + rcfThreshold: 0, + enterGate: address(0), + liquidatorGate: address(0) + }); + + vm.expectRevert(IMidnight.InvalidMaxLif.selector); + midnight.touchMarket(market); + } + // Default settlement fee tests function testSettlementFeeRevertsWhenNotCreated() public { @@ -296,7 +376,10 @@ contract SettersTest is BaseTest { // touch market with this loan token CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: loanToken, @@ -353,7 +436,10 @@ contract SettersTest is BaseTest { CollateralParams[] memory cols = new CollateralParams[](1); cols[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: address(0), @@ -407,7 +493,10 @@ contract SettersTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: address(loanToken), @@ -434,7 +523,10 @@ contract SettersTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: address(loanToken), @@ -467,7 +559,10 @@ contract SettersTest is BaseTest { CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ - token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) + token: address(collateralToken1), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, + oracle: address(oracle1) }); Market memory market = Market({ loanToken: address(loanToken), diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index b1f32e39..af1a5fef 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -8,7 +8,7 @@ import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; import {IMidnight, Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; -import {BaseTest, MAX_TEST_AMOUNT} from "./BaseTest.sol"; +import {BaseTest, LLTV, MAX_TEST_AMOUNT, LIQUIDATION_CURSOR} from "./BaseTest.sol"; // The maximum debt from a take must fit in uint128, and the required collateral (debt / lltv) // must also fit in uint128. With lltv = 0.75: collateral = debt * 4/3. @@ -43,8 +43,8 @@ contract SettlementFeeTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -52,8 +52,8 @@ contract SettlementFeeTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index d69a8b1b..420f52b8 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -6,7 +6,7 @@ import {Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; import {WAD, DEFAULT_TICK_SPACING} from "../src/libraries/ConstantsLib.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {TakeAmountsLib} from "../src/periphery/TakeAmountsLib.sol"; contract TakeAmountsTest is BaseTest { @@ -27,8 +27,8 @@ contract TakeAmountsTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -36,8 +36,8 @@ contract TakeAmountsTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/TakeTest.sol b/test/TakeTest.sol index 3d9abbc4..7b37cffd 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -11,7 +11,7 @@ import {IBuyCallback, ISellCallback} from "../src/interfaces/ICallbacks.sol"; import {IRatifier} from "../src/interfaces/IRatifier.sol"; import {IdLib} from "../src/libraries/IdLib.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; import {ERC20} from "./erc20s/ERC20.sol"; import {Oracle} from "./helpers/Oracle.sol"; import {StdStorage, stdStorage} from "../lib/forge-std/src/StdStorage.sol"; @@ -40,8 +40,8 @@ contract TakeTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -49,8 +49,8 @@ contract TakeTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index 015d1405..55da438b 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -7,7 +7,7 @@ import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; import {EventsLib} from "../src/libraries/EventsLib.sol"; -import {BaseTest} from "./BaseTest.sol"; +import {BaseTest, LLTV, LIQUIDATION_CURSOR} from "./BaseTest.sol"; /// @dev Integration tests for tick spacing enforcement in take() and spacing governance. contract TickGatingTest is BaseTest { @@ -25,8 +25,8 @@ contract TickGatingTest is BaseTest { .push( CollateralParams({ token: address(collateralToken1), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle1) }) ); @@ -34,8 +34,8 @@ contract TickGatingTest is BaseTest { .push( CollateralParams({ token: address(collateralToken2), - lltv: 0.77e18, - maxLif: maxLif(0.77e18, 0.25e18), + lltv: LLTV, + liquidationCursor: LIQUIDATION_CURSOR, oracle: address(oracle2) }) ); diff --git a/test/UtilsLibTest.sol b/test/UtilsLibTest.sol index d6fed580..f12eba43 100644 --- a/test/UtilsLibTest.sol +++ b/test/UtilsLibTest.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.0; import {Test, stdError} from "../lib/forge-std/src/Test.sol"; import {UtilsLib} from "../src/libraries/UtilsLib.sol"; import {LN_ONE_PLUS_DELTA, MAX_TICK, TickLib} from "../src/libraries/TickLib.sol"; -import {WAD, LIQUIDATION_CURSOR_LOW, LIQUIDATION_CURSOR_HIGH, maxLif} from "../src/libraries/ConstantsLib.sol"; -import {LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7} from "./BaseTest.sol"; contract UtilsLibTest is Test { int256 internal constant WEXP_LN2 = 0.693147180559945309e18; @@ -182,21 +180,4 @@ contract UtilsLibTest is Test { assertApproxEqRel(TickLib.wExp(19 ether), 178482300.96318726092869632 ether, 0.001 ether, "exp(19)"); assertApproxEqRel(TickLib.wExp(20 ether), 485165195.409790277969936384 ether, 0.001 ether, "exp(20)"); } - - // This test makes sure that the computation of maxRepaid (for RCF) is not too imprecise. - function testRcfBound() public pure { - uint256[8] memory lltvs = [LLTV_0, LLTV_1, LLTV_2, LLTV_3, LLTV_4, LLTV_5, LLTV_6, LLTV_7]; - uint256[2] memory cursors = [LIQUIDATION_CURSOR_LOW, LIQUIDATION_CURSOR_HIGH]; - for (uint256 i = 0; i < lltvs.length; i++) { - uint256 lltv = lltvs[i]; - for (uint256 j = 0; j < cursors.length; j++) { - uint256 lif = maxLif(lltv, cursors[j]); - assertLt(lif * lltv, WAD * WAD); - assertLt(WAD * WAD / (WAD * WAD - lif * lltv), 100); - } - } - // Also ensure that the bound is close to the max bound. - uint256 lifHigh7 = maxLif(LLTV_7, LIQUIDATION_CURSOR_HIGH); - assertGt(WAD * WAD / (WAD * WAD - lifHigh7 * LLTV_7), 90); - } } diff --git a/test/frontend/sign-root.ts b/test/frontend/sign-root.ts index 5235bed2..70ded229 100644 --- a/test/frontend/sign-root.ts +++ b/test/frontend/sign-root.ts @@ -28,7 +28,7 @@ function buildTypes(height: number) { CollateralParams: [ { name: "token", type: "address" }, { name: "lltv", type: "uint256" }, - { name: "maxLif", type: "uint256" }, + { name: "liquidationCursor", type: "uint256" }, { name: "oracle", type: "address" }, ], Market: [ @@ -63,7 +63,7 @@ function defaultOffer(number: string) { return { market: { loanToken: "0x" + number.repeat(40), - collateralParams: [{token: ZERO_ADDR, lltv: "0", maxLif: "0", oracle: ZERO_ADDR}], + collateralParams: [{token: ZERO_ADDR, lltv: "0", liquidationCursor: "0", oracle: ZERO_ADDR}], maturity: "0", rcfThreshold: "0", enterGate: ZERO_ADDR,