diff --git a/certora/confs/BalanceEffects.conf b/certora/confs/BalanceEffects.conf index c61ec3903..1cf8922ab 100644 --- a/certora/confs/BalanceEffects.conf +++ b/certora/confs/BalanceEffects.conf @@ -1,7 +1,11 @@ { "files": [ + "certora/helpers/Utils.sol", "src/Midnight.sol" ], + "parametric_contracts": [ + "Midnight" + ], "verify": "Midnight:certora/specs/BalanceEffects.spec", "solc": "solc-0.8.34", "solc_via_ir": true, diff --git a/certora/confs/Solvency.conf b/certora/confs/Solvency.conf index 886295bd1..a96ab4fe4 100644 --- a/certora/confs/Solvency.conf +++ b/certora/confs/Solvency.conf @@ -1,6 +1,7 @@ { "files": [ "src/Midnight.sol", + "certora/helpers/Utils.sol", "certora/helpers/FlashLiquidateCallback.sol" ], "parametric_contracts": [ diff --git a/certora/confs/WithdrawableMonotonicity.conf b/certora/confs/WithdrawableMonotonicity.conf index 08f85f116..92d89d1ba 100644 --- a/certora/confs/WithdrawableMonotonicity.conf +++ b/certora/confs/WithdrawableMonotonicity.conf @@ -1,7 +1,11 @@ { "files": [ + "certora/helpers/Utils.sol", "src/Midnight.sol" ], + "parametric_contracts": [ + "Midnight" + ], "verify": "Midnight:certora/specs/WithdrawableMonotonicity.spec", "solc": "solc-0.8.34", "solc_via_ir": true, diff --git a/certora/helpers/Utils.sol b/certora/helpers/Utils.sol index af7b56e96..709a5d57f 100644 --- a/certora/helpers/Utils.sol +++ b/certora/helpers/Utils.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Offer, Market} from "../../src/interfaces/IMidnight.sol"; import {UtilsLib} from "../../src/libraries/UtilsLib.sol"; +import {IdLib} from "../../src/libraries/IdLib.sol"; import { CALLBACK_SUCCESS, LIQUIDATION_CURSOR_LOW, @@ -14,6 +15,10 @@ import { } from "../../src/libraries/ConstantsLib.sol"; contract Utils { + function toId(Market memory market) external pure returns (bytes32) { + return IdLib.toId(market); + } + function hashMarket(Market memory market) external pure returns (bytes32) { return keccak256(abi.encode(market)); } diff --git a/certora/specs/BalanceEffects.spec b/certora/specs/BalanceEffects.spec index 67fe2380a..0d70660ed 100644 --- a/certora/specs/BalanceEffects.spec +++ b/certora/specs/BalanceEffects.spec @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function credit(bytes32 id, address user) external returns (uint128) envfree; @@ -12,7 +15,7 @@ methods { function continuousFeeCredit(bytes32 id) external returns (uint128) envfree; // Summarize internals irrelevant to credit and debt tracking. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; @@ -34,7 +37,7 @@ methods { /// sets it to the post-update value, only changes credit of user at the market id, /// and accrues fee to continuousFeeCredit. rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 creditBefore = credit(id, user); uint128 updatedUserCredit; @@ -61,7 +64,7 @@ rule updatePositionEffects(env e, Midnight.Market market, address user, bytes32 /// withdraw decreases onBehalf's post-update credit by exactly units /// and only changes credit of onBehalf at the market id. rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address receiver, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint128 updatedUserCredit; uint128 userFee; @@ -84,7 +87,7 @@ rule withdrawEffects(env e, Midnight.Market market, uint256 units, address onBeh /// take changes maker's and taker's net credit-debt by +/- units relative to their post-update values /// and only changes credit of maker and taker and debt of maker and taker at the market id. rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, offer.market); + bytes32 id = Utils.toId(offer.market); uint128 makerCreditBefore; makerCreditBefore, _, _ = updatePositionView(e, offer.market, id, offer.maker); @@ -112,7 +115,7 @@ rule takeEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, /// Buyer's credit is non-decreasing relative to its post-update value and can increase by at most take units. /// Buyer's debt is non-increasing and can decrease by at most take units. rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = toId(e, offer.market); + bytes32 id = Utils.toId(offer.market); address buyer = offer.buy ? offer.maker : taker; uint256 buyerDebtBefore = debt(id, buyer); @@ -132,7 +135,7 @@ rule takeBuyerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 u /// Seller's debt is non-decreasing, and can increase by at most take units. /// Seller's credit is non-increasing relative to its post-update value and can decrease by at most take units. rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData) { - bytes32 id = toId(e, offer.market); + bytes32 id = Utils.toId(offer.market); address seller = offer.buy ? taker : offer.maker; uint256 sellerDebtBefore = debt(id, seller); @@ -152,7 +155,7 @@ rule takeSellerEffects(env e, Midnight.Offer offer, bytes ratifierData, uint256 /// Repay decreases onBehalf's debt by exactly units and only changes position[id][onBehalf].debt rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data, bytes32 anyId, address anyUser) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 debtBefore = debt(id, onBehalf); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -170,7 +173,7 @@ rule repayEffects(env e, Midnight.Market market, uint256 units, address onBehalf /// Liquidate decreases the borrower's debt by at least repaidUnits, /// and only changes position[id][borrower].debt. rule liquidateEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bytes32 anyId, address anyUser, bool postMaturityMode) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 debtBefore = debt(id, borrower); uint256 otherCreditBefore = credit(anyId, anyUser); @@ -209,7 +212,7 @@ filtered { /// supplyCollateral increases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -225,7 +228,7 @@ rule supplyCollateralEffects(env e, Midnight.Market market, uint256 collateralIn /// withdrawCollateral decreases onBehalf's collateral by exactly assets, /// and only changes position[id][onBehalf].collateral[collateralIndex]. rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 assets, address onBehalf, address receiver, bytes32 anyId, address anyUser, uint256 anyIndex) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, onBehalf, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); @@ -241,7 +244,7 @@ rule withdrawCollateralCollateralEffects(env e, Midnight.Market market, uint256 /// liquidate decreases the borrower's collateral at collateralIndex by exactly seizedResult, /// and only changes position[id][borrower].collateral[collateralIndex]. rule liquidateCollateralEffects(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bytes32 anyId, address anyUser, uint256 anyIndex, bool postMaturityMode) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 collateralBefore = collateral(id, borrower, collateralIndex); uint256 otherCollateralBefore = collateral(anyId, anyUser, anyIndex); diff --git a/certora/specs/BundlerRepayInvertibility.spec b/certora/specs/BundlerRepayInvertibility.spec index e1ccd3ce6..7967e6ebf 100644 --- a/certora/specs/BundlerRepayInvertibility.spec +++ b/certora/specs/BundlerRepayInvertibility.spec @@ -10,7 +10,7 @@ methods { function midnight.tickSpacing(bytes32 id) external returns (uint8) envfree; // Deterministic market id (same pattern as Midnight.spec / TakeAmountsLibInvertibility.spec). - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic mulDivDown. function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivDown(x, y, d); diff --git a/certora/specs/CollateralBitmap.spec b/certora/specs/CollateralBitmap.spec index 82acdd8d4..a74cc8277 100644 --- a/certora/specs/CollateralBitmap.spec +++ b/certora/specs/CollateralBitmap.spec @@ -18,7 +18,7 @@ methods { */ function _.price() external => PER_CALLEE_CONSTANT; function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => NONDET; /* Simplify mulDiv reasoning for the solver. We summarize these by ghost functions, i.e., * arbitrary deterministic functions and axiomatize the axioms we need. diff --git a/certora/specs/Consume.spec b/certora/specs/Consume.spec index 5af605c5c..d15a14659 100644 --- a/certora/specs/Consume.spec +++ b/certora/specs/Consume.spec @@ -7,7 +7,7 @@ methods { function totalUnits(bytes32 id) external returns (uint128) envfree; // Summaries for complex internals irrelevant to consumed-mapping properties. - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; function UtilsLib.mulDivDown(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivUp(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/ContinuousFee.spec b/certora/specs/ContinuousFee.spec index 847f314de..d86e601cd 100644 --- a/certora/specs/ContinuousFee.spec +++ b/certora/specs/ContinuousFee.spec @@ -3,7 +3,7 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => CVL_toId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => CVL_toId(market); function credit(bytes32 id, address user) external returns (uint128) envfree; function pendingFee(bytes32 id, address user) external returns (uint128) envfree; @@ -11,7 +11,7 @@ methods { function continuousFeeCredit(bytes32 id) external returns (uint128) envfree; // Summarize internals irrelevant to continuous fee tracking. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; diff --git a/certora/specs/CreatedMarkets.spec b/certora/specs/CreatedMarkets.spec index 62000bf77..59583afa7 100644 --- a/certora/specs/CreatedMarkets.spec +++ b/certora/specs/CreatedMarkets.spec @@ -25,10 +25,10 @@ methods { function UtilsLib.mulDivDown(uint256 x, uint256 y, uint256 d) internal returns (uint256) => ghostMulDivDown(x, y, d); // Summary is required because abi.encodePacked doesn't ensure injectivity of the hash function in CVL, for an unknown reason. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound because the protocol doesn't use toMarket. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Tokens are assumed to not reenter, for performance reasons. function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; @@ -130,7 +130,17 @@ rule marketIsCreatedAfterLiquidate(env e, Midnight.Market market, uint256 collat assert marketIsCreated(market); } -// Markets can only be created by: touchMarket, take, withdraw, repay, supplyCollateral, withdrawCollateral or liquidate. +rule marketIsCreatedAfterClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver) { + claimContinuousFee(e, market, amount, receiver); + assert marketIsCreated(market); +} + +rule marketIsCreatedAfterUpdatePosition(env e, Midnight.Market market, address user) { + updatePosition(e, market, user); + assert marketIsCreated(market); +} + +// Markets can only be created by: touchMarket, take, withdraw, repay, supplyCollateral, withdrawCollateral, liquidate, claimContinuousFee or updatePosition. rule onlyTouchMarketCreatesMarket(env e, method f, calldataarg args, Midnight.Market market) filtered { f -> f.selector != sig:touchMarket(Midnight.Market).selector @@ -140,6 +150,8 @@ filtered { && f.selector != sig:supplyCollateral(Midnight.Market, uint256, uint256, address).selector && f.selector != sig:withdrawCollateral(Midnight.Market, uint256, uint256, address, address).selector && f.selector != sig:liquidate(Midnight.Market, uint256, uint256, uint256, address, bool, address, address, bytes).selector + && f.selector != sig:claimContinuousFee(Midnight.Market, uint256, address).selector + && f.selector != sig:updatePosition(Midnight.Market, address).selector } { require !marketIsCreated(market), "Assume that the market is not created"; f(e, args); diff --git a/certora/specs/EmptyOffer.spec b/certora/specs/EmptyOffer.spec index 22309b982..77b2045d9 100644 --- a/certora/specs/EmptyOffer.spec +++ b/certora/specs/EmptyOffer.spec @@ -7,7 +7,7 @@ methods { function Utils.emptyOffer() external returns (Midnight.Offer) envfree; // Summarize internals, which is sound since it would only remove revert reasons. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/Healthiness.spec b/certora/specs/Healthiness.spec index bdf41b042..93c2b5245 100644 --- a/certora/specs/Healthiness.spec +++ b/certora/specs/Healthiness.spec @@ -16,7 +16,7 @@ methods { // Under this assumption we can prove that a healthy borrower cannot get unhealthy by any action on the contract. function _.price() external => summaryPrice(calledContract) expect(uint256); function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Summarize mulDivDown and mulDivUp to simplify the verification task. // Use a ghost function that ensures mulDivDown/Up behaves deterministically and add only the axioms about mulDiv that are needed to prove the desired property. @@ -25,7 +25,7 @@ methods { function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivUp(x, y, d); function _.havocAll() external => HAVOC_ALL; - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function SafeTransferLib.safeTransfer(address, address, uint256) internal => transferCallback(); function SafeTransferLib.safeTransferFrom(address, address, address, uint256) internal => transferCallback(); @@ -102,6 +102,8 @@ persistent ghost bool healthyOrLockedBeforeCallbacks; // global variable to track which market and borrower we're testing. persistent ghost address globalMarketLoanToken; +persistent ghost uint256 globalMarketChainId; + persistent ghost uint256 globalMarketCollateralLength; persistent ghost mapping(uint256 => address) globalMarketCollateralOracle; @@ -129,7 +131,7 @@ persistent ghost address globalBorrower; definition collateralMatches(Midnight.Market market, uint256 index) returns bool = (index < globalMarketCollateralLength => market.collateralParams[index].oracle == globalMarketCollateralOracle[index] && market.collateralParams[index].token == globalMarketCollateralToken[index] && market.collateralParams[index].lltv == globalMarketCollateralLLTV[index] && market.collateralParams[index].maxLif == globalMarketCollateralMaxLif[index]); function equalsGlobalMarket(Midnight.Market market) returns (bool) { - return market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; + return market.chainId == globalMarketChainId && market.midnight == currentContract && market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; } function getGlobalMarket() returns (Midnight.Market) { @@ -138,9 +140,9 @@ function getGlobalMarket() returns (Midnight.Market) { return market; } -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns (bytes32) { +function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && midnight == currentContract) { + if (equalsGlobalMarket(market)) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; diff --git a/certora/specs/Liquidate.spec b/certora/specs/Liquidate.spec index 4ca0919c2..dcdce416a 100644 --- a/certora/specs/Liquidate.spec +++ b/certora/specs/Liquidate.spec @@ -18,14 +18,14 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => summaryMulDivUp(a, b, denominator); // IdLib summary: remember the last id returned by toId. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); } /// HELPERS /// persistent ghost bytes32 liqId; -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns bytes32 { +function summaryToId(Midnight.Market market) returns bytes32 { bytes32 id; liqId = id; return id; diff --git a/certora/specs/LiquidationBoundedByLIF.spec b/certora/specs/LiquidationBoundedByLIF.spec index efd665e99..b8376cc74 100644 --- a/certora/specs/LiquidationBoundedByLIF.spec +++ b/certora/specs/LiquidationBoundedByLIF.spec @@ -14,10 +14,10 @@ methods { function _.price() external => summaryPrice(calledContract) expect(uint256); // Deterministic toId summary using a wrapper that extracts all scalar Market fields. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Skip market creation logic: removes the collateral-validation loop. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Token transfers happen after return values are computed; irrelevant to the assertion. function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/LiquidationProfitability.spec b/certora/specs/LiquidationProfitability.spec index 220e1067d..64334730f 100644 --- a/certora/specs/LiquidationProfitability.spec +++ b/certora/specs/LiquidationProfitability.spec @@ -15,10 +15,10 @@ methods { function UtilsLib.mulDivUp(uint256 x, uint256 y, uint256 d) internal returns (uint256) => summaryMulDivUp(x, y, d); // Deterministic toId summary using a wrapper that extracts all scalar Market fields. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Skip market creation logic: removes the collateral-validation loop. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Token transfers happen after return values are computed; irrelevant to the assertion. function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/LossFactor.spec b/certora/specs/LossFactor.spec index 4ac816dc5..beebf46be 100644 --- a/certora/specs/LossFactor.spec +++ b/certora/specs/LossFactor.spec @@ -14,8 +14,8 @@ methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Deterministic toId needed to link market arguments to stored state. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // SafeTransferLib summaries: bypass transfer logic (needed for liquidate @withrevert rules). function SafeTransferLib.safeTransfer(address, address, uint256) internal => NONDET; diff --git a/certora/specs/Midnight.spec b/certora/specs/Midnight.spec index a8731d849..fbe67720b 100644 --- a/certora/specs/Midnight.spec +++ b/certora/specs/Midnight.spec @@ -15,8 +15,8 @@ methods { function tickSpacing(bytes32 id) external returns (uint8) envfree; function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; diff --git a/certora/specs/NoDivisionByZero.spec b/certora/specs/NoDivisionByZero.spec index 1eb8c1c5f..f0b2efeaa 100644 --- a/certora/specs/NoDivisionByZero.spec +++ b/certora/specs/NoDivisionByZero.spec @@ -19,7 +19,7 @@ methods { function _.price() external => ghostPrice(calledContract) expect(uint256); // Summary for deterministic toId for the global market. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => summaryToId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // This function is checked manually to not cause a division by zero. function TickLib.tickToPrice(uint256) internal returns (uint256) => NONDET; @@ -35,6 +35,8 @@ methods { persistent ghost address globalMarketLoanToken; +persistent ghost uint256 globalMarketChainId; + persistent ghost uint256 globalMarketCollateralLength; persistent ghost mapping(uint256 => address) globalMarketCollateralOracle; @@ -71,12 +73,12 @@ definition WAD() returns uint256 = 10 ^ 18; definition collateralMatches(Midnight.Market market, uint256 index) returns bool = (index < globalMarketCollateralLength => market.collateralParams[index].oracle == globalMarketCollateralOracle[index] && market.collateralParams[index].token == globalMarketCollateralToken[index] && market.collateralParams[index].lltv == globalMarketCollateralLLTV[index] && market.collateralParams[index].maxLif == globalMarketCollateralMaxLif[index]); function equalsGlobalMarket(Midnight.Market market) returns (bool) { - return market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; + return market.chainId == globalMarketChainId && market.midnight == currentContract && market.loanToken == globalMarketLoanToken && market.collateralParams.length == globalMarketCollateralLength && collateralMatches(market, 0) && collateralMatches(market, 1) && collateralMatches(market, 2) && market.maturity == globalMarketMaturity && market.rcfThreshold == globalMarketRcfThreshold && market.enterGate == globalMarketEnterGate && market.liquidatorGate == globalMarketLiquidatorGate; } -function summaryToId(Midnight.Market market, uint256 chainId, address midnight) returns (bytes32) { +function summaryToId(Midnight.Market market) returns (bytes32) { bytes32 id; - if (equalsGlobalMarket(market) && midnight == currentContract) { + if (equalsGlobalMarket(market)) { require id == globalId, "toId() is deterministic"; } else { require id != globalId, "toId() is injective"; diff --git a/certora/specs/NoMultiplicationOverflow.spec b/certora/specs/NoMultiplicationOverflow.spec index 51ba4fd21..b7ef4f146 100644 --- a/certora/specs/NoMultiplicationOverflow.spec +++ b/certora/specs/NoMultiplicationOverflow.spec @@ -14,7 +14,7 @@ methods { function _.price() external => boundedPrice(calledContract) expect(uint256); // Deterministic toId: links call-site markets to validated state from touchMarket. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound return bound: tickToPrice <= WAD for non-reverting calls. function TickLib.tickToPrice(uint256) internal returns (uint256) => boundedTickPrice(); diff --git a/certora/specs/OnlyAuthorizedCanChange.spec b/certora/specs/OnlyAuthorizedCanChange.spec index 03b6ef455..bd802cb98 100644 --- a/certora/specs/OnlyAuthorizedCanChange.spec +++ b/certora/specs/OnlyAuthorizedCanChange.spec @@ -3,7 +3,6 @@ methods { function multicall(bytes[]) external => HAVOC_ALL DELETE; - function toId(Midnight.Market market) external returns (bytes32) envfree; function credit(bytes32 id, address user) external returns (uint128) envfree; function debt(bytes32 id, address user) external returns (uint128) envfree; function collateral(bytes32 id, address user, uint256 index) external returns (uint128) envfree; @@ -11,7 +10,7 @@ methods { function isAuthorized(address authorizer, address authorized) external returns (bool) envfree; // Summarize internal functions that use opcodes causing HAVOC (CREATE2, low-level calls). - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; diff --git a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec index 22dc0aa47..a32cb5b3d 100644 --- a/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec +++ b/certora/specs/OnlyAuthorizedCanChangeUpdatedValues.spec @@ -16,10 +16,10 @@ methods { function lossFactor(bytes32) external returns (uint128) envfree; // Summarize toId to be able to reference the id in the rules. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Sound because the protocol doesn't use toMarket. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; diff --git a/certora/specs/PostMaturityDebt.spec b/certora/specs/PostMaturityDebt.spec index ee689b83d..de6de2aa1 100644 --- a/certora/specs/PostMaturityDebt.spec +++ b/certora/specs/PostMaturityDebt.spec @@ -9,7 +9,7 @@ methods { function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; // Deterministic toId summary. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Callbacks and token transfers reached by take/repay/liquidate/flashLoan use the default AUTO summary // (HAVOC_ECF), which assumes the callees do not re-enter Midnight and so leave the debt unchanged. diff --git a/certora/specs/Ratification.spec b/certora/specs/Ratification.spec index 6ec782598..cd02755e4 100644 --- a/certora/specs/Ratification.spec +++ b/certora/specs/Ratification.spec @@ -6,7 +6,7 @@ methods { function isAuthorized(address authorizer, address authorized) external returns (bool) envfree; // Over-approximate view functions. - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; function UtilsLib.mulDivDown(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivUp(uint256, uint256, uint256) internal returns (uint256) => NONDET; function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; diff --git a/certora/specs/Reverts.spec b/certora/specs/Reverts.spec index 2dfcccde1..952428e43 100644 --- a/certora/specs/Reverts.spec +++ b/certora/specs/Reverts.spec @@ -45,10 +45,10 @@ methods { function SafeTransferLib.safeTransfer(address, address, uint256) internal => CVL_safeTransfer(); // Bitmap operations (msb, clearBit, setBit) are provided by BitmapSummaries.spec. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // The function toMarket is not used by the protocol. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; function TickLib.tickToPrice(uint256) internal returns (uint256) => NONDET; function UtilsLib.mulDivDown(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => CVL_mulDivDown(a, b, denominator); diff --git a/certora/specs/Role.spec b/certora/specs/Role.spec index a8206c670..5ccaba4a1 100644 --- a/certora/specs/Role.spec +++ b/certora/specs/Role.spec @@ -3,6 +3,7 @@ using Utils as Utils; methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function roleSetter() external returns (address) envfree; @@ -270,9 +271,9 @@ rule feeClaimerCanClaimSettlementFee(env e, address token, uint256 amount, addre } rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amount, address receiver, address user) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); address feeClaimerBefore = feeClaimer(); - bool marketIsCreated = marketIsCreated(id); + bool marketWasCreated = marketIsCreated(id); uint256 withdrawableBefore = withdrawable(id); uint256 totalUnitsBefore = totalUnits(id); uint128 continuousFeeCreditBefore = currentContract.marketState[id].continuousFeeCredit; @@ -282,7 +283,8 @@ rule feeClaimerCanClaimContinuousFee(env e, Midnight.Market market, uint256 amou claimContinuousFee@withrevert(e, market, amount, receiver); bool reverted = lastReverted; - assert !reverted <=> e.msg.sender == feeClaimerBefore && e.msg.value == 0 && marketIsCreated && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore; + assert !reverted => e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore; + assert marketWasCreated && e.msg.sender == feeClaimerBefore && e.msg.value == 0 && amount <= withdrawableBefore && amount <= totalUnitsBefore && amount <= continuousFeeCreditBefore => !reverted; assert !reverted => withdrawable(id) == withdrawableBefore - amount; assert !reverted => totalUnits(id) == totalUnitsBefore - amount; assert !reverted => currentContract.marketState[id].continuousFeeCredit == continuousFeeCreditBefore - amount; diff --git a/certora/specs/SettlementFeeBoundaries.spec b/certora/specs/SettlementFeeBoundaries.spec index f924bc177..8463fac6c 100644 --- a/certora/specs/SettlementFeeBoundaries.spec +++ b/certora/specs/SettlementFeeBoundaries.spec @@ -3,12 +3,12 @@ using Utils as Utils; methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function settlementFee(bytes32 id, uint256 timeToMaturity) external returns (uint256) envfree; function feeSetter() external returns (address) envfree; function tickSpacing(bytes32 id) external returns (uint8) envfree; - function toId(Midnight.Market) external returns (bytes32) envfree; function Utils.maxSettlementFee(uint256 index) external returns (uint256) envfree; // Over-approximate view functions. @@ -43,6 +43,12 @@ invariant marketSettlementFeePerIndexBound(bytes32 id, uint256 index) preserved touchMarket(Midnight.Market market) with (env e) { requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); } + preserved claimContinuousFee(Midnight.Market market, uint256 amount, address receiver) with (env e) { + requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); + } + preserved updatePosition(Midnight.Market market, address user) with (env e) { + requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); + } preserved withdraw(Midnight.Market market, uint256 units, address onBehalf, address receiver) with (env e) { requireInvariant defaultSettlementFeePerIndexBound(market.loanToken, index); } @@ -66,7 +72,7 @@ invariant marketSettlementFeePerIndexBound(bytes32 id, uint256 index) /// When a market is created, its settlement fees are set to the default settlement fees of its loan token. rule newMarketSettlementFeesMatchDefault(env e, Midnight.Market market, uint256 index) { require index <= 6, "index out of bounds"; - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); require tickSpacing(id) == 0, "market not yet created"; uint256 expectedSettlementFee = defaultSettlementFee(market.loanToken, index); diff --git a/certora/specs/SettlementFeeSpread.spec b/certora/specs/SettlementFeeSpread.spec index dba07ea3a..2eb7e8b5e 100644 --- a/certora/specs/SettlementFeeSpread.spec +++ b/certora/specs/SettlementFeeSpread.spec @@ -9,13 +9,13 @@ methods { function settlementFee(bytes32, uint256) external returns (uint256) envfree; // Summary is required because abi.encodePacked doesn't ensure injectivity of the hash function in CVL, for an unknown reason. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic TickLib.tickToPrice summary to be able to reference the price in the rules. function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => summaryTickToPrice(tick); // Sound summary since toMarket is not used by the protocol. - function IdLib.storeInCode(Midnight.Market memory, uint256) internal returns (address) => NONDET; + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Over-approximate view functions for prover performance. function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; diff --git a/certora/specs/Solvency.spec b/certora/specs/Solvency.spec index 4202619aa..4b34e22e9 100644 --- a/certora/specs/Solvency.spec +++ b/certora/specs/Solvency.spec @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function claimableSettlementFee(address token) external returns (uint256) envfree; @@ -10,7 +13,7 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 denominator) internal returns (uint256) => CVL_mulDivUp(a, b, denominator); // Summarize toId, this adds no assumption but allows to retrieve the loan token from the market id. - function IdLib.toId(Midnight.Market memory market, uint256 chainId, address midnight) internal returns (bytes32) => CVL_toId(market, chainId, midnight); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => CVL_toId(market); // Summaries for complex internals irrelevant to token balance tracking. function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; @@ -73,11 +76,11 @@ ghost mapping(bytes32 => address) loantoken; // Mapping from market id and collateral index to the corresponding collateral token. ghost mapping(bytes32 => mapping(uint128 => address)) collateralToken; -ghost hash(address, uint256, uint256, address) returns bytes32; +ghost hash(bytes32) returns bytes32; -function CVL_toId(Midnight.Market market, uint256 chainId, address midnight) returns bytes32 { - // Deterministically derive the market id. - bytes32 id = hash(market.loanToken, market.maturity, chainId, midnight); +function CVL_toId(Midnight.Market market) returns bytes32 { + // Deterministically derive an arbitrary market id from the full market. + bytes32 id = hash(Utils.hashMarket(market)); // Assume the market id already maps to this loan token. // We could also initialize on first use, but then token(0) handling needs extra constraints. diff --git a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec index 859a7384a..49a45af29 100644 --- a/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec +++ b/certora/specs/SplitDoesNotPunishMakerOrFavorTaker.spec @@ -13,10 +13,10 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 d) internal returns (uint256) => ghostMulDivUp(a, b, d); // Summarize toId: deterministic hash preserves market-to-id relationship without adding assumptions. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Assume that the markets are already created. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Pure helper called with identical args across the three takes; CONSTANT collapses // its bit / hashing / arithmetic complexity (no behavioral abstraction). diff --git a/certora/specs/SplitPreservesAccounting.spec b/certora/specs/SplitPreservesAccounting.spec index ed2681548..715cad6c9 100644 --- a/certora/specs/SplitPreservesAccounting.spec +++ b/certora/specs/SplitPreservesAccounting.spec @@ -18,10 +18,10 @@ methods { function UtilsLib.mulDivUp(uint256 a, uint256 b, uint256 d) internal returns (uint256) => ghostMulDivUp(a, b, d); // Deterministic hash preserves market-to-id relationship without adding assumptions. - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); - // Assume that the markets are already created. - function touchMarket(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); + // Sound because the protocol doesn't use toMarket. + function IdLib.storeInCode(Midnight.Market memory) internal returns (address) => NONDET; // Pure helper called with identical args across the three takes; CONSTANT collapses // its bit / hashing / arithmetic complexity (no behavioral abstraction). diff --git a/certora/specs/TakeAmountsLibInvertibility.spec b/certora/specs/TakeAmountsLibInvertibility.spec index b8599b3b8..9d02fb0dd 100644 --- a/certora/specs/TakeAmountsLibInvertibility.spec +++ b/certora/specs/TakeAmountsLibInvertibility.spec @@ -13,8 +13,8 @@ methods { function TakeAmountsLibHarness.buyerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); function TakeAmountsLibHarness.sellerAssetsToUnits(address, bytes32, Midnight.Offer, uint256) external returns (uint256); - // Deterministic id: same Market => same id. Matches what take() and TakeAmountsLib observe (both reach IdLib.toId via Midnight.toId / Midnight.touchMarket). - function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); + // Deterministic toId function. + function IdLib.toId(Midnight.Market memory market) internal returns (bytes32) => summaryToId(market); // Deterministic price: the same tick yields the same price for both the library and take(). function TickLib.tickToPrice(uint256 tick) internal returns (uint256) => summaryTickToPrice(tick); diff --git a/certora/specs/UpdateBeforeCredit.spec b/certora/specs/UpdateBeforeCredit.spec index 323eeaf67..d5d7b1089 100644 --- a/certora/specs/UpdateBeforeCredit.spec +++ b/certora/specs/UpdateBeforeCredit.spec @@ -10,7 +10,7 @@ methods { function UtilsLib.msb(uint128) internal returns (uint256) => NONDET; function UtilsLib.countBits(uint128) internal returns (uint256) => NONDET; - function IdLib.toId(Midnight.Market memory, uint256, address) internal returns (bytes32) => NONDET; + function IdLib.toId(Midnight.Market memory) internal returns (bytes32) => NONDET; // Summarize _updatePosition so that its credit reads/writes do not fire the hooks below. function _updatePosition(Midnight.Market memory, bytes32 id, address user) internal returns (uint128, uint128, uint128) => summaryUpdatePosition(id, user); diff --git a/certora/specs/WithdrawableMonotonicity.spec b/certora/specs/WithdrawableMonotonicity.spec index 046572958..00c23f9bb 100644 --- a/certora/specs/WithdrawableMonotonicity.spec +++ b/certora/specs/WithdrawableMonotonicity.spec @@ -1,15 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later +using Utils as Utils; + methods { + function Utils.toId(Midnight.Market) external returns (bytes32) envfree; function multicall(bytes[]) external => HAVOC_ALL DELETE; function withdrawable(bytes32 id) external returns (uint128) envfree; function claimableSettlementFee(address token) external returns (uint256) envfree; - function toId(Midnight.Market) external returns (bytes32); } rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, address onBehalf, address callback, bytes data) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); repay(e, market, units, onBehalf, callback, data); uint256 withdrawableAfter = withdrawable(id); @@ -17,7 +19,7 @@ rule repayIncreasesWithdrawable(env e, Midnight.Market market, uint256 units, ad } rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, address receiver, address callback, bytes data, bool postMaturityMode) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); uint256 seizedResult; uint256 repaidResult; @@ -27,7 +29,7 @@ rule liquidateIncreasesWithdrawable(env e, Midnight.Market market, uint256 colla } rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 unitsInput, address onBehalf, address receiver) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); withdraw(e, market, unitsInput, onBehalf, receiver); uint256 withdrawableAfter = withdrawable(id); @@ -35,7 +37,7 @@ rule withdrawDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 } rule claimContinuousFeeDecreasesWithdrawableExactly(env e, Midnight.Market market, uint256 amount, address receiver) { - bytes32 id = toId(e, market); + bytes32 id = Utils.toId(market); uint256 withdrawableBefore = withdrawable(id); claimContinuousFee(e, market, amount, receiver); uint256 withdrawableAfter = withdrawable(id); diff --git a/src/Midnight.sol b/src/Midnight.sol index 2f6fc6e7f..1bdbebe63 100644 --- a/src/Midnight.sol +++ b/src/Midnight.sol @@ -179,9 +179,7 @@ import {IMidnight, Market, Offer, CollateralParams, MarketState, Position} from /// @dev credit, pendingFee, and lastLossFactor are not up to date. Use updatePositionView to get the up-to-date /// values. /// @dev The max amount of totalUnits, collateral, credit, continuousFeeCredit and debt is type(uint128).max (~1e38). -/// @dev INITIAL_CHAIN_ID is captured at construction and used in place of block.chainid when computing market ids, -/// so a hard fork that changes block.chainid does not strand existing accounting. But as a result, after a hard-fork -/// there can be some market id clashes. +/// @dev Markets use their creation chainId, so after a chain fork two markets on different chains can have the same id. /// @dev When selecting offers ("routing"), one should take into consideration the gas associated with their callbacks. /// @dev Relies on the clz opcode (Osaka), on the mcopy, tload, and tstore opcodes (Cancun), and on the push0 opcode /// (Shanghai). @@ -190,10 +188,6 @@ contract Midnight is IMidnight { using UtilsLib for uint256; using UtilsLib for uint128; - /// IMMUTABLES /// - - uint256 public immutable INITIAL_CHAIN_ID; - /// STORAGE /// mapping(bytes32 id => mapping(address user => Position)) public position; @@ -213,8 +207,7 @@ contract Midnight is IMidnight { constructor() { roleSetter = msg.sender; - INITIAL_CHAIN_ID = block.chainid; - emit EventsLib.Constructor(msg.sender, INITIAL_CHAIN_ID); + emit EventsLib.Constructor(msg.sender); } /// MULTICALL /// @@ -329,10 +322,9 @@ contract Midnight is IMidnight { } function claimContinuousFee(Market memory market, uint256 amount, address receiver) external { - bytes32 id = toId(market); + bytes32 id = touchMarket(market); MarketState storage _marketState = marketState[id]; require(msg.sender == feeClaimer, OnlyFeeClaimer()); - require(_marketState.tickSpacing > 0, MarketNotCreated()); _marketState.continuousFeeCredit -= UtilsLib.toUint128(amount); _marketState.totalUnits -= UtilsLib.toUint128(amount); @@ -778,8 +770,10 @@ contract Midnight is IMidnight { /// @dev Returns the market id and creates the market if it doesn't exist yet. function touchMarket(Market memory market) public returns (bytes32) { - bytes32 id = toId(market); + bytes32 id = IdLib.toId(market); if (marketState[id].tickSpacing == 0) { + require(market.chainId == block.chainid, InvalidChainId()); + require(market.midnight == address(this), InvalidMidnight()); require(market.maturity <= block.timestamp + 100 * 365 days, MaturityTooFar()); require(market.collateralParams.length > 0, NoCollateralParams()); require(market.collateralParams.length <= MAX_COLLATERALS, TooManyCollateralParams()); @@ -808,7 +802,7 @@ contract Midnight is IMidnight { _marketState.settlementFeeCbp5 = _defaultSettlementFeeCbp[5]; _marketState.settlementFeeCbp6 = _defaultSettlementFeeCbp[6]; _marketState.continuousFee = defaultContinuousFee[market.loanToken]; - IdLib.storeInCode(market, INITIAL_CHAIN_ID); + IdLib.storeInCode(market); emit EventsLib.MarketCreated(market, id); } @@ -846,8 +840,7 @@ contract Midnight is IMidnight { /// @dev Slashes the position and accrues the continuous fee. /// @dev Returns the new credit, new pending fee, and accrued fee after having updated the position. function updatePosition(Market memory market, address user) external returns (uint128, uint128, uint128) { - bytes32 id = toId(market); - require(marketState[id].tickSpacing > 0, MarketNotCreated()); + bytes32 id = touchMarket(market); return _updatePosition(market, id, user); } @@ -909,10 +902,6 @@ contract Midnight is IMidnight { return position[id][user].collateral[index]; } - function toId(Market memory market) public view returns (bytes32) { - return IdLib.toId(market, INITIAL_CHAIN_ID, address(this)); - } - /// @dev Reverts if the id is not a valid id of a touched market. /// @dev Returns the market corresponding to the given id. function toMarket(bytes32 id) external view returns (Market memory) { diff --git a/src/interfaces/IMidnight.sol b/src/interfaces/IMidnight.sol index 7d93054da..ff7e5acf4 100644 --- a/src/interfaces/IMidnight.sol +++ b/src/interfaces/IMidnight.sol @@ -3,6 +3,8 @@ pragma solidity >=0.5.0; struct Market { + uint256 chainId; + address midnight; address loanToken; CollateralParams[] collateralParams; uint256 maturity; @@ -76,9 +78,11 @@ interface IMidnight { error ContinuousFeeAboveOfferCap(); error FeeNotMultipleOfFeeCbp(); error InconsistentInput(); + error InvalidChainId(); error InvalidFeeIndex(); error InvalidLltv(); error InvalidMaxLif(); + error InvalidMidnight(); error InvalidOfferCaps(); error InvalidTickSpacing(); error LiquidatorGatedFromLiquidating(); @@ -117,9 +121,6 @@ interface IMidnight { error WrongSellCallbackReturnValue(); // forgefmt: disable-start - /// IMMUTABLES /// - function INITIAL_CHAIN_ID() external view returns (uint256); - /// STORAGE GETTERS /// function position(bytes32 id, address user) external view returns (uint128 credit, uint128 pendingFee, uint128 lastLossFactor, uint128 lastAccrual, uint128 debt, uint128 collateralBitmap); function marketState(bytes32 id) external view returns (uint128 totalUnits, uint128 lossFactor, uint128 withdrawable, uint128 continuousFeeCredit, uint16 settlementFeeCbp0, uint16 settlementFeeCbp1, uint16 settlementFeeCbp2, uint16 settlementFeeCbp3, uint16 settlementFeeCbp4, uint16 settlementFeeCbp5, uint16 settlementFeeCbp6, uint32 continuousFee, uint8 tickSpacing); @@ -171,7 +172,6 @@ interface IMidnight { function lastLossFactor(bytes32 id, address user) external view returns (uint128); function collateralBitmap(bytes32 id, address user) external view returns (uint128); function collateral(bytes32 id, address user, uint256 index) external view returns (uint128); - function toId(Market memory market) external view returns (bytes32); function toMarket(bytes32 id) external view returns (Market memory); function credit(bytes32 id, address user) external view returns (uint128); function debt(bytes32 id, address user) external view returns (uint128); diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index 6fb835273..edcb54878 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -7,7 +7,7 @@ import {Market} from "../interfaces/IMidnight.sol"; /// @dev id_ is used to avoid naming conflicts in indexers. library EventsLib { // forgefmt: disable-start - event Constructor(address indexed roleSetter, uint256 initialChainId); + event Constructor(address indexed roleSetter); event SetRoleSetter(address indexed roleSetter); event SetFeeSetter(address indexed feeSetter); event SetTickSpacingSetter(address indexed tickSpacingSetter); diff --git a/src/libraries/IdLib.sol b/src/libraries/IdLib.sol index 85586d13f..5f6348ffb 100644 --- a/src/libraries/IdLib.sol +++ b/src/libraries/IdLib.sol @@ -22,20 +22,22 @@ library IdLib { /// f3 RETURN [] mem[0:len] is returned bytes constant SSTORE2_PREFIX = hex"600b380380600b5f395ff3"; - function toId(Market memory market, uint256 chainId, address midnight) internal pure returns (bytes32) { + function toId(Market memory market) internal pure returns (bytes32) { return keccak256( abi.encodePacked( - uint8(0xff), midnight, chainId, keccak256(abi.encodePacked(SSTORE2_PREFIX, abi.encode(market))) + uint8(0xff), + market.midnight, + uint256(0), + keccak256(abi.encodePacked(SSTORE2_PREFIX, abi.encode(market))) ) ); } - /// @dev Stores the data in the code of the contract at the given address. - /// @dev Uses the given chain id as salt. - function storeInCode(Market memory market, uint256 chainId) internal returns (address create2Address) { + /// @dev Stores the market in the code of the contract at the address given by the last 20 bytes of its id. + function storeInCode(Market memory market) internal returns (address create2Address) { bytes memory creationCode = abi.encodePacked(SSTORE2_PREFIX, abi.encode(market)); assembly ("memory-safe") { - create2Address := create2(0, add(creationCode, 0x20), mload(creationCode), chainId) + create2Address := create2(0, add(creationCode, 0x20), mload(creationCode), 0) } require(create2Address != address(0), SStore2DeploymentFailed()); } diff --git a/src/periphery/MidnightBundles.sol b/src/periphery/MidnightBundles.sol index 9e0823130..f2c574a6c 100644 --- a/src/periphery/MidnightBundles.sol +++ b/src/periphery/MidnightBundles.sol @@ -15,6 +15,7 @@ import { import {IERC20Permit} from "./interfaces/IERC20Permit.sol"; import {IPermit2} from "./interfaces/IPermit2.sol"; import {UtilsLib} from "../libraries/UtilsLib.sol"; +import {IdLib} from "../libraries/IdLib.sol"; import {SafeTransferLib} from "../libraries/SafeTransferLib.sol"; import {TakeAmountsLib} from "./TakeAmountsLib.sol"; import {ConsumableUnitsLib} from "./ConsumableUnitsLib.sol"; @@ -80,7 +81,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledBuyerAssets; for (uint256 i; i < takes.length && filledUnits < targetUnits; i++) { require(!takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( targetUnits - filledUnits, takes[i].units, @@ -147,7 +148,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledSellerAssets; for (uint256 i; i < takes.length && filledUnits < targetUnits; i++) { require(takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( targetUnits - filledUnits, takes[i].units, @@ -204,7 +205,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledBuyerAssets; for (uint256 i; i < takes.length && filledBuyerAssets < targetFilledBuyerAssets; i++) { require(!takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( TakeAmountsLib.buyerAssetsToUnits( MIDNIGHT, id, takes[i].offer, targetFilledBuyerAssets - filledBuyerAssets @@ -275,7 +276,7 @@ contract MidnightBundles is IMidnightBundles { uint256 filledSellerAssets; for (uint256 i; i < takes.length && filledSellerAssets < targetFilledSellerAssets; i++) { require(takes[i].offer.buy, InconsistentSide()); - require(IMidnight(MIDNIGHT).toId(takes[i].offer.market) == id, InconsistentMarket()); + require(IdLib.toId(takes[i].offer.market) == id, InconsistentMarket()); uint256 unitsToTake = min( TakeAmountsLib.sellerAssetsToUnits( MIDNIGHT, id, takes[i].offer, targetFilledSellerAssets - filledSellerAssets diff --git a/src/ratifiers/libraries/HashLib.sol b/src/ratifiers/libraries/HashLib.sol index 13d8be011..e480226c0 100644 --- a/src/ratifiers/libraries/HashLib.sol +++ b/src/ratifiers/libraries/HashLib.sol @@ -7,9 +7,9 @@ import {Offer, Market, CollateralParams} from "../../interfaces/IMidnight.sol"; /// @dev keccak256("CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"). bytes32 constant COLLATERAL_PARAMS_TYPEHASH = 0xaf44a88eb50ebdbbebd980e5a23045c44f61ece5f80ab708a1bbe8718102e6af; /// @dev keccak256(bytes.concat(MARKET_TYPE, COLLATERAL_PARAMS_TYPE)). -bytes32 constant MARKET_TYPEHASH = 0x358117e98511cc3df97175dca58053b06675b43ad090b0553f8a1eff008b6e2e; +bytes32 constant MARKET_TYPEHASH = 0xc66c045aa2394a02e2976962976ec58c79108ae7fbb1ecc974c9724678b56264; /// @dev keccak256(bytes.concat(OFFER_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). -bytes32 constant OFFER_TYPEHASH = 0x6bd2a06ec6952feb97c3e3b4f7de6c342f12b1ac769d5c91368271af636c85b7; +bytes32 constant OFFER_TYPEHASH = 0x5e7c764a0f2411d16dd65139c973cbe0fe976b6d0736823e17aef319f652e7f8; library HashLib { error LeafIndexOutOfRange(); @@ -21,28 +21,28 @@ library HashLib { /// @dev Reverts if height is greater than 20. function offerTreeTypeHash(uint256 height) internal pure returns (bytes32) { if (height <= 10) { - if (height == 0) return 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 0x04931ea05149e935551af887e04668a4235aa7fefe5a1307699fc36de5da3604; + if (height == 1) return 0x636eec88d23afdd7c5ecd3689cddd4a578f3640022a4f4b36532b1bb873870d9; + if (height == 2) return 0xb3b200d8a87156dd298add6829190724360e913ea2a544cf8770a83b1a85b68e; + if (height == 3) return 0x9959f9ee7df42c7c3a42ae48edb3544cb9672653eb4c9ad5190aa9ac194e13cc; + if (height == 4) return 0x859439b1a679d2b8d78d092af16a3e3abd30ccae9d29e12183c61dbd89069798; + if (height == 5) return 0x3b62f568d54c69f46baa3db29d9d16daa670a421eca545015e2c4d6ac5e2ab4b; + if (height == 6) return 0x06b027b3e518caa75c38007c1e4cffc92d314d528eb7ed63b28f6cbb250d6221; + if (height == 7) return 0x1948d59b3835088b65d9f7048f26cfcf508f9a750de32e7830b6f99c8915905a; + if (height == 8) return 0xfb12ec1a894c5cc36be99898bcc4c23d45713b8920d21ddf94ba0053fec835b4; + if (height == 9) return 0xee136fb578b4eea50b5929361278f0a09f97371d5c44ebb73b07785150f3c202; + return 0xdbd2c72ce0b8a438efba0500c50090d842804420fa9eb5819a265105ccf446d4; } else { - if (height == 11) return 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 0x8201cbae421a17b9d8116ce28ecd13378b22d0257e9daca65ea354dc7b852e0a; + if (height == 12) return 0xb76e7eedf6cb4dfd2a913d292be9bbcdcdf6bc457e789306cc23a6917faca3d2; + if (height == 13) return 0x911f1aca18bfbc4e142c04b2020d972507d4b9d6b2fbef339bc48d33a438be9e; + if (height == 14) return 0xf8b8028014cfa85c41a0b5af6dd4d5e6c7236dab5886ee4f21e845ec4205443d; + if (height == 15) return 0xd517d3bc505b0209f539cf985e6252f0e57975ec5c8e3a93d97366f39c8cbbc5; + if (height == 16) return 0x35940ca810d4b02086a4d695dfaf04195c5a0fcb776e97c1349a8a0472b7bec8; + if (height == 17) return 0x44c0e3ddcf369c808911a5c78a3d7e87fa115a23cc40147b5fc40064c5560213; + if (height == 18) return 0x032fe7574172aadd1fb6c40e9deacfec41a48e2fc8de3574c67331c10e7b3a1f; + if (height == 19) return 0x3f80439b44bef469ff2390bdf0fd5469db472bb6748c6de9e5e2cd86d602c301; + if (height == 20) return 0x61068a1b8ffb6dd774e8a9634cb46a7a214dd84189e255072577c8541c543fd4; revert TreeTooHigh(); } } @@ -103,6 +103,8 @@ library HashLib { return keccak256( abi.encode( MARKET_TYPEHASH, + market.chainId, + market.midnight, market.loanToken, collateralParamsHash, market.maturity, diff --git a/test/AuthorizationTest.sol b/test/AuthorizationTest.sol index f901bdab5..a1eb9d21e 100644 --- a/test/AuthorizationTest.sol +++ b/test/AuthorizationTest.sol @@ -19,6 +19,8 @@ contract AuthorizationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/BaseTest.sol b/test/BaseTest.sol index a662137ee..3a09501d3 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -244,8 +244,8 @@ abstract contract BaseTest is Test { Oracle(market.collateralParams[0].oracle).setPrice(ORACLE_PRICE_SCALE); } - function toId(Market memory market) internal view returns (bytes32) { - return IdLib.toId(market, block.chainid, address(midnight)); + function toId(Market memory market) internal pure returns (bytes32) { + return IdLib.toId(market); } function domainSeparator(address verifyingContract) internal view returns (bytes32) { @@ -297,6 +297,8 @@ abstract contract BaseTest is Test { collateralParams[i].maxLif = maxLif(lltv, LIQUIDATION_CURSOR_LOW); } collateralParams = sortCollateralParams(collateralParams); + market.chainId = block.chainid; + market.midnight = address(midnight); market.collateralParams = collateralParams; market.maturity = bound(market.maturity, 0, vm.getBlockTimestamp() + 100 * 365 days); return market; diff --git a/test/ContinuousFeeTest.sol b/test/ContinuousFeeTest.sol index 3507e525a..090716dc1 100644 --- a/test/ContinuousFeeTest.sol +++ b/test/ContinuousFeeTest.sol @@ -23,6 +23,8 @@ contract ContinuousFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100 days; market.collateralParams .push( @@ -558,15 +560,24 @@ contract ContinuousFeeTest is BaseTest { assertEq(midnight.continuousFeeCredit(id), expectedContinuousFeeCredit, "continuousFeeCredit"); } - function testUpdatePositionRevertsIfMarketNotCreated() public { - vm.expectRevert(IMidnight.MarketNotCreated.selector); - midnight.updatePosition(market, borrower); + function testUpdatePositionTouchesMarketIfNotCreated() public { + assertEq(midnight.tickSpacing(id), 0, "market not created before"); + + (uint128 credit, uint128 pendingFee, uint128 accruedFee) = midnight.updatePosition(market, borrower); + + assertEq(credit, 0, "credit"); + assertEq(pendingFee, 0, "pendingFee"); + assertEq(accruedFee, 0, "accruedFee"); + assertGt(midnight.tickSpacing(id), 0, "market created after"); } - function testClaimContinuousFeeRevertsIfMarketNotCreated() public { + function testClaimContinuousFeeTouchesMarketIfNotCreated() public { + assertEq(midnight.tickSpacing(id), 0, "market not created before"); + vm.prank(feeClaimer); - vm.expectRevert(IMidnight.MarketNotCreated.selector); midnight.claimContinuousFee(market, 0, feeClaimer); + + assertGt(midnight.tickSpacing(id), 0, "market created after"); } function testLastAccrualZeroForFreshPosition() public { diff --git a/test/EcrecoverRatifierIntegrationTest.sol b/test/EcrecoverRatifierIntegrationTest.sol index 0c1776d72..4aa5bf153 100644 --- a/test/EcrecoverRatifierIntegrationTest.sol +++ b/test/EcrecoverRatifierIntegrationTest.sol @@ -25,6 +25,8 @@ contract EcrecoverRatifierIntegrationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/FrontendSignatureTest.sol b/test/FrontendSignatureTest.sol index df675cbe5..dcbfed083 100644 --- a/test/FrontendSignatureTest.sol +++ b/test/FrontendSignatureTest.sol @@ -9,10 +9,10 @@ import {CALLBACK_SUCCESS} from "../src/libraries/ConstantsLib.sol"; import {HashLib} from "../src/ratifiers/libraries/HashLib.sol"; // Paste from frontend output. -address constant ACCOUNT = 0xFDa6883171208B36122229505FB2D6F30c052311; +address constant ACCOUNT = 0x6133Fd1B38C7D9ad120a6fC0D0f7b03d4F6E9658; uint8 constant SIG_V = 28; -bytes32 constant SIG_R = 0xb7a8c34b3aa87d799f2bd6e01a36c6a48673313015c592fc4137043b37ee80c6; -bytes32 constant SIG_S = 0x5161ce684b17e81a0b297441856e8d8b498c541116fd6dde5d87e5624b847afb; +bytes32 constant SIG_R = 0x70edf1a8f911f5f66575f3f9a2f8a4ac5b7ce80da66fe6a9f1d3a1f527fb7d04; +bytes32 constant SIG_S = 0x32e9f29d8f20c1116b3c1484b203d2e5e6c295161de512569ca5502fd110cb36; address constant RATIFIER = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; @@ -25,6 +25,8 @@ contract FrontendSignatureTest is Test { function defaultOffer(uint8 number) internal pure returns (Offer memory offer) { CollateralParams[] memory collateralParams = new CollateralParams[](1); + offer.market.chainId = 1; + offer.market.midnight = address(0); offer.market.loanToken = address(uint160(0x1111111111111111111111111111111111111111) * uint160(number)); offer.market.collateralParams = collateralParams; offer.expiry = 2 ** 32; diff --git a/test/GateTest.sol b/test/GateTest.sol index f587e8125..a4d9936fd 100644 --- a/test/GateTest.sol +++ b/test/GateTest.sol @@ -43,6 +43,8 @@ contract GateTest is BaseTest { gate = new WhitelistGate(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -56,6 +58,8 @@ contract GateTest is BaseTest { market.collateralParams = sortCollateralParams(market.collateralParams); gatedMarket.loanToken = address(loanToken); + gatedMarket.chainId = block.chainid; + gatedMarket.midnight = address(midnight); gatedMarket.maturity = vm.getBlockTimestamp() + 100; gatedMarket.collateralParams .push( diff --git a/test/HashLibTest.sol b/test/HashLibTest.sol index 42f5b5e9b..1369a6f34 100644 --- a/test/HashLibTest.sol +++ b/test/HashLibTest.sol @@ -12,7 +12,7 @@ import {Market} from "../src/interfaces/IMidnight.sol"; bytes constant COLLATERAL_PARAMS_TYPE = "CollateralParams(address token,uint256 lltv,uint256 maxLif,address oracle)"; bytes constant MARKET_TYPE = - "Market(address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; + "Market(uint256 chainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)"; bytes constant OFFER_TYPE = "Offer(Market market,bool buy,address maker,uint256 start,uint256 expiry,uint256 tick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,address ratifier,bool reduceOnly,uint256 maxUnits,uint256 maxAssets,uint256 continuousFeeCap)"; @@ -37,6 +37,8 @@ contract HashLibTest is Test { bytes32 expectedHash = keccak256( abi.encode( MARKET_TYPEHASH, + market.chainId, + market.midnight, market.loanToken, keccak256(abi.encodePacked(collateralParamsHashes)), market.maturity, diff --git a/test/IdLibTest.sol b/test/IdLibTest.sol index 01d59ad7c..909afce5b 100644 --- a/test/IdLibTest.sol +++ b/test/IdLibTest.sol @@ -3,63 +3,67 @@ pragma solidity ^0.8.0; import {Test} from "../lib/forge-std/src/Test.sol"; import {IdLib} from "../src/libraries/IdLib.sol"; -import {Market} from "../src/interfaces/IMidnight.sol"; +import {Market, CollateralParams} from "../src/interfaces/IMidnight.sol"; // toMarket is tested in OtherFunctionsTest.sol, to test actual implementation (avoid introducing mocks). contract IdLibTest is Test { - function testToIdIsInjectiveInMarket( - Market memory market1, - Market memory market2, - uint256 chainid, - address midnight - ) public pure { - bool sameLoanToken = market1.loanToken == market2.loanToken; - bool sameMaturity = market1.maturity == market2.maturity; - bool sameCollaterals = market1.collateralParams.length == market2.collateralParams.length; - bool sameRcfThreshold = market1.rcfThreshold == market2.rcfThreshold; - if (sameCollaterals) { - for (uint256 i = 0; i < market1.collateralParams.length; i++) { - if (market1.collateralParams[i].token != market2.collateralParams[i].token) { - sameCollaterals = false; - } - if (market1.collateralParams[i].lltv != market2.collateralParams[i].lltv) { - sameCollaterals = false; - } - if (market1.collateralParams[i].maxLif != market2.collateralParams[i].maxLif) { - sameCollaterals = false; - } - if (market1.collateralParams[i].oracle != market2.collateralParams[i].oracle) { - sameCollaterals = false; - } - } - } + function baseMarket() internal pure returns (Market memory market) { + market.chainId = 1; + market.midnight = address(1); + market.loanToken = address(2); + market.collateralParams = new CollateralParams[](1); + market.collateralParams[0] = + CollateralParams({token: address(3), lltv: 0.77e18, maxLif: 1.1e18, oracle: address(4)}); + market.maturity = 123; + market.rcfThreshold = 456; + market.enterGate = address(5); + market.liquidatorGate = address(6); + } - vm.assume(!(sameLoanToken && sameMaturity && sameCollaterals && sameRcfThreshold)); + function testToIdIsSensitiveToMarketParams() public pure { + Market memory market1 = baseMarket(); + Market memory market2 = baseMarket(); + market2.chainId = 2; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - bytes32 id1 = IdLib.toId(market1, chainid, midnight); - bytes32 id2 = IdLib.toId(market2, chainid, midnight); - assertNotEq(id1, id2); - } + market2 = baseMarket(); + market2.midnight = address(7); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - function testToIdIsInjectiveInChainId(Market memory market, uint256 chainid1, uint256 chainid2, address midnight) - public - pure - { - vm.assume(chainid1 != chainid2); - bytes32 id1 = IdLib.toId(market, chainid1, midnight); - bytes32 id2 = IdLib.toId(market, chainid2, midnight); - assertNotEq(id1, id2); - } + market2 = baseMarket(); + market2.loanToken = address(8); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].token = address(9); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].lltv = 0.5e18; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].maxLif = 1.2e18; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.collateralParams[0].oracle = address(12); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.maturity = 789; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.rcfThreshold = 789; + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); + + market2 = baseMarket(); + market2.enterGate = address(10); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); - function testToIdIsInjectiveInMidnight( - Market memory market, - uint256 chainid, - address midnightOne, - address midnightTwo - ) public pure { - vm.assume(midnightOne != midnightTwo); - bytes32 id1 = IdLib.toId(market, chainid, midnightOne); - bytes32 id2 = IdLib.toId(market, chainid, midnightTwo); - assertNotEq(id1, id2); + market2 = baseMarket(); + market2.liquidatorGate = address(11); + assertNotEq(IdLib.toId(market1), IdLib.toId(market2)); } } diff --git a/test/LiquidationTest.sol b/test/LiquidationTest.sol index 330dc54e0..5fb0116f8 100644 --- a/test/LiquidationTest.sol +++ b/test/LiquidationTest.sol @@ -45,6 +45,8 @@ contract LiquidationTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -1014,7 +1016,7 @@ contract LiquidationTest is BaseTest { bytes memory data, uint256 badDebt ) public returns (bytes32) { - require(_id == IdLib.toId(_market, block.chainid, msg.sender), "wrong id"); + require(_id == IdLib.toId(_market), "wrong id"); recordedCaller = _caller; recordedId = _id; recordedMarket = _market; diff --git a/test/MaxAmountsTest.sol b/test/MaxAmountsTest.sol index fe1cefae8..9b5592f60 100644 --- a/test/MaxAmountsTest.sol +++ b/test/MaxAmountsTest.sol @@ -20,6 +20,8 @@ contract MaxAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/MidnightBundlesTest.sol b/test/MidnightBundlesTest.sol index 47f2a8443..551c108b2 100644 --- a/test/MidnightBundlesTest.sol +++ b/test/MidnightBundlesTest.sol @@ -46,6 +46,8 @@ contract MidnightBundlesTest is BaseTest { } market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/OtherFunctionsTest.sol b/test/OtherFunctionsTest.sol index 08827607d..210b33484 100644 --- a/test/OtherFunctionsTest.sol +++ b/test/OtherFunctionsTest.sol @@ -43,6 +43,8 @@ contract OtherFunctionsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -310,6 +312,8 @@ contract OtherFunctionsTest is BaseTest { bytes32 _id = midnight.touchMarket(_market); Market memory marketFromId = midnight.toMarket(_id); + assertEq(_market.chainId, marketFromId.chainId, "chainId"); + assertEq(_market.midnight, marketFromId.midnight, "midnight"); assertEq(_market.loanToken, marketFromId.loanToken, "loanToken"); assertEq(_market.maturity, marketFromId.maturity, "maturity"); assertEq(_market.collateralParams.length, marketFromId.collateralParams.length, "collateralParams length"); @@ -321,11 +325,11 @@ contract OtherFunctionsTest is BaseTest { } } - function testToId(Market memory _market) public view { + function testIdLibToId(Market memory _market) public view { _market = validMarket(_market); bytes32 expected = toId(_market); - bytes32 actual = midnight.toId(_market); + bytes32 actual = IdLib.toId(_market); assertEq(actual, expected, "toId mismatch"); } @@ -335,12 +339,9 @@ contract OtherFunctionsTest is BaseTest { _market = validMarket(_market); bytes32 idBefore = midnight.touchMarket(_market); - uint256 capturedChainId = midnight.INITIAL_CHAIN_ID(); - vm.chainId(newChainId); - assertEq(midnight.INITIAL_CHAIN_ID(), capturedChainId, "INITIAL_CHAIN_ID changed"); - assertEq(midnight.toId(_market), idBefore, "toId changed"); + assertEq(toId(_market), idBefore, "toId changed"); Market memory roundTrip = midnight.toMarket(idBefore); assertEq(keccak256(abi.encode(roundTrip)), keccak256(abi.encode(_market)), "stored market lost"); @@ -355,6 +356,24 @@ contract OtherFunctionsTest is BaseTest { midnight.toMarket(_id); } + function testTouchMarketInvalidChainId(Market memory _market) public { + vm.assume(_market.collateralParams.length > 0); + _market = validMarket(_market); + _market.chainId = block.chainid + 1; + + vm.expectRevert(IMidnight.InvalidChainId.selector); + midnight.touchMarket(_market); + } + + function testTouchMarketInvalidMidnight(Market memory _market) public { + vm.assume(_market.collateralParams.length > 0); + _market = validMarket(_market); + _market.midnight = address(0); + + vm.expectRevert(IMidnight.InvalidMidnight.selector); + midnight.touchMarket(_market); + } + function testSstore2CodeStartsWithStop(Market memory _market) public { vm.assume(_market.collateralParams.length > 0); _market = validMarket(_market); @@ -379,6 +398,8 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.chainId = block.chainid; + marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -403,6 +424,8 @@ contract OtherFunctionsTest is BaseTest { Market memory marketWithRevertingOracle; marketWithRevertingOracle.loanToken = address(loanToken); + marketWithRevertingOracle.chainId = block.chainid; + marketWithRevertingOracle.midnight = address(midnight); marketWithRevertingOracle.maturity = vm.getBlockTimestamp() + 100; marketWithRevertingOracle.collateralParams = collateralParams; @@ -431,6 +454,8 @@ contract OtherFunctionsTest is BaseTest { } collateralParams = sortCollateralParams(collateralParams); _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = collateralParams; _market.rcfThreshold = 0; @@ -440,6 +465,8 @@ contract OtherFunctionsTest is BaseTest { maturity = bound(maturity, vm.getBlockTimestamp() + 100 * 365 days + 1, type(uint256).max); Market memory longMarket; longMarket.loanToken = address(loanToken); + longMarket.chainId = block.chainid; + longMarket.midnight = address(midnight); longMarket.maturity = maturity; longMarket.collateralParams = market.collateralParams; @@ -450,6 +477,8 @@ contract OtherFunctionsTest is BaseTest { function testZeroCollaterals() public { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; _market.collateralParams = new CollateralParams[](0); vm.expectRevert(IMidnight.NoCollateralParams.selector); @@ -480,6 +509,8 @@ contract OtherFunctionsTest is BaseTest { function testCollateralsNotSorted() public { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](2); collateralParams[0] = CollateralParams({ @@ -497,6 +528,8 @@ contract OtherFunctionsTest is BaseTest { lltv = bound(lltv, WAD + 1, type(uint256).max); Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -512,6 +545,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.5e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -648,6 +683,8 @@ contract OtherFunctionsTest is BaseTest { Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = @@ -662,6 +699,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 100; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -677,6 +716,8 @@ contract OtherFunctionsTest is BaseTest { uint256 lltv = 0.77e18; Market memory _market; _market.loanToken = address(loanToken); + _market.chainId = block.chainid; + _market.midnight = address(midnight); _market.maturity = vm.getBlockTimestamp() + 200; CollateralParams[] memory collateralParams = new CollateralParams[](1); collateralParams[0] = CollateralParams({ @@ -781,7 +822,7 @@ contract RepayCallback { external returns (bytes32) { - require(marketId == IdLib.toId(market, block.chainid, msg.sender), "wrong marketId"); + require(marketId == IdLib.toId(market), "wrong marketId"); recordedId = marketId; _recordedMarket = market; recordedData = data; diff --git a/test/SetterRatifierTest.sol b/test/SetterRatifierTest.sol index b374aef86..092538f46 100644 --- a/test/SetterRatifierTest.sol +++ b/test/SetterRatifierTest.sol @@ -21,6 +21,8 @@ contract SetterRatifierTest is BaseTest { function makeOffer(address maker) internal view returns (Offer memory offer) { Market memory market; market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams = new CollateralParams[](1); market.collateralParams[0] = CollateralParams({ diff --git a/test/SettersTest.sol b/test/SettersTest.sol index bcd8ac21a..c840d2139 100644 --- a/test/SettersTest.sol +++ b/test/SettersTest.sol @@ -35,7 +35,7 @@ contract SettersTest is BaseTest { function testConstructorEvent() public { vm.expectEmit(); - emit EventsLib.Constructor(address(this), block.chainid); + emit EventsLib.Constructor(address(this)); new Midnight(); } @@ -125,6 +125,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, collateralParams: collateralParams, @@ -299,6 +301,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: loanToken, maturity: vm.getBlockTimestamp() + 1 days, collateralParams: collateralParams, @@ -356,6 +360,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: address(0), maturity: vm.getBlockTimestamp() + 1 days, collateralParams: cols, @@ -410,6 +416,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, @@ -437,6 +445,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, @@ -470,6 +480,8 @@ contract SettersTest is BaseTest { token: address(collateralToken1), lltv: 0.77e18, maxLif: maxLif(0.77e18, 0.25e18), oracle: address(oracle1) }); Market memory market = Market({ + chainId: block.chainid, + midnight: address(midnight), loanToken: address(loanToken), maturity: vm.getBlockTimestamp() + 100 days, collateralParams: collateralParams, diff --git a/test/SettlementFeeTest.sol b/test/SettlementFeeTest.sol index b1f32e399..4c8cd0fe3 100644 --- a/test/SettlementFeeTest.sol +++ b/test/SettlementFeeTest.sol @@ -38,6 +38,8 @@ contract SettlementFeeTest is BaseTest { vm.warp(vm.getBlockTimestamp() + 1000 days); // to be able to come back in time enough market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 1 days; // TTM = 1 day (exactly at breakpoint) market.collateralParams .push( diff --git a/test/TakeAmountsTest.sol b/test/TakeAmountsTest.sol index d69a8b1b1..853fedfea 100644 --- a/test/TakeAmountsTest.sol +++ b/test/TakeAmountsTest.sol @@ -22,6 +22,8 @@ contract TakeAmountsTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/TakeTest.sol b/test/TakeTest.sol index 3d9abbc40..3afc6c134 100644 --- a/test/TakeTest.sol +++ b/test/TakeTest.sol @@ -35,6 +35,8 @@ contract TakeTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( @@ -1512,7 +1514,7 @@ contract BorrowCallback is ISellCallback { address receiver, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); recordedId = id; _recordedMarket = market; recordedSeller = seller; @@ -1547,7 +1549,7 @@ contract ReentrantLiquidateBorrowCallback is ISellCallback { address, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); (uint256 collateralIndex, uint256 collateralAmount, uint256 repaidUnits) = abi.decode(data, (uint256, uint256, uint256)); address collateralToken = market.collateralParams[collateralIndex].token; @@ -1604,7 +1606,7 @@ contract NestedTakeReentrantLiquidateCallback is ISellCallback { external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); if (!reentered) { uint256 idx = storedCollateralIndex; address collateralToken = market.collateralParams[idx].token; @@ -1653,7 +1655,7 @@ contract LendCallback is IBuyCallback { address buyer, bytes memory data ) external returns (bytes32) { - require(id == IdLib.toId(market, block.chainid, msg.sender), "wrong id"); + require(id == IdLib.toId(market), "wrong id"); recordedId = id; _recordedMarket = market; recordedBuyer = buyer; diff --git a/test/TickGatingTest.sol b/test/TickGatingTest.sol index 015d1405a..3db63593c 100644 --- a/test/TickGatingTest.sol +++ b/test/TickGatingTest.sol @@ -20,6 +20,8 @@ contract TickGatingTest is BaseTest { super.setUp(); market.loanToken = address(loanToken); + market.chainId = block.chainid; + market.midnight = address(midnight); market.maturity = vm.getBlockTimestamp() + 100; market.collateralParams .push( diff --git a/test/frontend/sign-root.ts b/test/frontend/sign-root.ts index 5235bed28..817845cf8 100644 --- a/test/frontend/sign-root.ts +++ b/test/frontend/sign-root.ts @@ -32,6 +32,8 @@ function buildTypes(height: number) { { name: "oracle", type: "address" }, ], Market: [ + { name: "chainId", type: "uint256" }, + { name: "midnight", type: "address" }, { name: "loanToken", type: "address" }, { name: "collateralParams", type: "CollateralParams[]" }, { name: "maturity", type: "uint256" }, @@ -62,6 +64,8 @@ function buildTypes(height: number) { function defaultOffer(number: string) { return { market: { + chainId: "1", + midnight: ZERO_ADDR, loanToken: "0x" + number.repeat(40), collateralParams: [{token: ZERO_ADDR, lltv: "0", maxLif: "0", oracle: ZERO_ADDR}], maturity: "0",