-
Notifications
You must be signed in to change notification settings - Fork 45
[Certora] Net credit after pending fee stays constant #988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jhoenicke
wants to merge
11
commits into
main
Choose a base branch
from
certora-credit-nondecreasing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+248
−0
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
2d1a043
Rules for netCredit is not decreasing.
jhoenicke 99076fb
Strenghthened property to ==, added pending==0 after maturity
jhoenicke 5e5c7d9
Added toId() summary and one missing require
jhoenicke c42533d
axiomatized mulDiv
jhoenicke 28d3ddd
remove non-linear bottleneck
jhoenicke 18981c0
avoid calling toId twice
jhoenicke 9ffc55d
Apply suggestions from code review
jhoenicke 15c9c0c
Review comments
jhoenicke c9aabd6
[Certora] net credit refactor (#1001)
QGarchery 088ced4
Apply suggestions from code review
jhoenicke 64da25e
Require MAX_CONTINUOUS_FEE and MAX_TTM separately
jhoenicke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "files": [ | ||
| "certora/helpers/Utils.sol", | ||
| "src/Midnight.sol" | ||
| ], | ||
| "verify": "Midnight:certora/specs/NetCredit.spec", | ||
| "solc": "solc-0.8.34", | ||
| "solc_via_ir": true, | ||
| "solc_evm_version": "osaka", | ||
| "optimistic_loop": true, | ||
| "loop_iter": 2, | ||
| "optimistic_hashing": true, | ||
| "hashing_length_bound": 2048, | ||
| "smt_timeout": 7200, | ||
| "multi_assert_check": true, | ||
| "prover_args": [ | ||
| "-destructiveOptimizations twostage" | ||
| ], | ||
| "msg": "NetCredit" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| using Utils as Utils; | ||
|
|
||
| methods { | ||
| function multicall(bytes[]) external => HAVOC_ALL DELETE; | ||
|
|
||
| function lossFactor(bytes32 id) external returns (uint128) envfree; | ||
| function pendingFee(bytes32 id, address user) external returns (uint128) envfree; | ||
| function creditOf(bytes32 id, address user) external returns (uint128) envfree; | ||
| function lastAccrual(bytes32 id, address user) external returns (uint128) envfree; | ||
| function lastLossFactor(bytes32 id, address user) external returns (uint128) envfree; | ||
| function continuousFee(bytes32 id) external returns (uint32) envfree; | ||
| function toId(Midnight.Market) external returns (bytes32) envfree; | ||
| function Utils.hashMarket(Midnight.Market) external returns (bytes32) envfree; | ||
|
|
||
| // Deterministic hash preserves market-to-id relationship without adding assumptions. | ||
| function IdLib.toId(Midnight.Market memory market, uint256, address) internal returns (bytes32) => summaryToId(market); | ||
|
QGarchery marked this conversation as resolved.
|
||
|
|
||
| // 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. | ||
| 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); | ||
|
|
||
| // Over-approximate view functions. | ||
| function TickLib.tickToPrice(uint256) internal returns (uint256) => NONDET; | ||
| function settlementFee(bytes32, uint256) internal returns (uint256) => NONDET; | ||
| function isHealthy(Midnight.Market memory, bytes32, address) internal returns (bool) => NONDET; | ||
| } | ||
|
|
||
| /// HELPERS /// | ||
|
|
||
| definition MAX_CONTINUOUS_FEE() returns uint256 = 317097919; | ||
|
|
||
| definition MAX_TTM() returns mathint = 100 * 365 * 86400; | ||
|
|
||
| definition WAD() returns uint256 = 10 ^ 18; | ||
|
|
||
| definition zeroFloorSub(uint256 a, uint256 b) returns mathint = a >= b ? a - b : 0; | ||
|
|
||
| /// SUMMARY FUNCTIONS /// | ||
|
|
||
| persistent ghost ghostMulDivDown(uint256, uint256, uint256) returns uint256 { | ||
| /* proved in mulDivZero in MulDiv.spec */ | ||
| axiom forall uint256 b. forall uint256 d. d > 0 => ghostMulDivDown(0, b, d) == 0; | ||
| axiom forall uint256 a. forall uint256 d. d > 0 => ghostMulDivDown(a, 0, d) == 0; | ||
|
|
||
| /* proved in mulDivIdentity in MulDiv.spec */ | ||
| axiom forall uint256 a. forall uint256 b. b > 0 => ghostMulDivDown(a, b, b) == a; | ||
|
|
||
| /* proved in mulDivArgumentLesserThanDenominator in MulDiv.spec */ | ||
| axiom forall uint256 a. forall uint256 b. forall uint256 d. d > 0 && b <= d => ghostMulDivDown(a, b, d) <= a; | ||
| } | ||
|
|
||
| persistent ghost ghostMulDivUp(uint256, uint256, uint256) returns uint256 { | ||
| /* proved in mulDivZero in MulDiv.spec */ | ||
| axiom forall uint256 a. forall uint256 d. d > 0 => ghostMulDivUp(a, 0, d) == 0; | ||
|
|
||
| /* proved in mulDivArgumentLesserThanDenominator in MulDiv.spec */ | ||
| axiom forall uint256 a. forall uint256 b. forall uint256 d. d > 0 && a <= d => ghostMulDivUp(a, b, d) <= b; | ||
|
|
||
| /* proved in mulDivResidualBound in MulDiv.spec */ | ||
| axiom forall uint256 a. forall uint256 b. forall uint256 d. a <= d && b <= d => a - ghostMulDivUp(a, b, d) <= d - b; | ||
| } | ||
|
|
||
| function summaryMulDivDown(uint256 a, uint256 b, uint256 d) returns uint256 { | ||
| bool overflow; | ||
| if (overflow || d == 0) { | ||
| revert(); | ||
| } | ||
| return ghostMulDivDown(a, b, d); | ||
| } | ||
|
|
||
| function summaryMulDivUp(uint256 a, uint256 b, uint256 d) returns uint256 { | ||
| bool overflow; | ||
| if (overflow || d == 0) { | ||
| revert(); | ||
| } | ||
| return ghostMulDivUp(a, b, d); | ||
| } | ||
|
|
||
| function summaryToId(Midnight.Market market) returns (bytes32) { | ||
| return Utils.hashMarket(market); | ||
| } | ||
|
|
||
| /// The up-to-date face value of a lender's position: credit - pendingFee after slashing and fee accrual. | ||
| function netCredit(env e, Midnight.Market market, address user) returns mathint { | ||
| bytes32 id = toId(market); | ||
| uint128 credit; | ||
| uint128 pending; | ||
| uint128 accruedFee; | ||
| require pendingFee(id, user) <= creditOf(id, user), "See pendingContinuousFeeBoundedByCredit in Midnight.spec"; | ||
| require lastLossFactor(id, user) <= lossFactor(id), "See lastLossFactorLeqMarketLossFactor in Midnight.spec"; | ||
| credit, pending, accruedFee = updatePositionView(e, market, id, user); | ||
|
QGarchery marked this conversation as resolved.
QGarchery marked this conversation as resolved.
|
||
| return credit - pending; | ||
| } | ||
|
|
||
| /// INVARIANTS /// | ||
|
|
||
| /// Once a position has been accrued at or after maturity, its pending fee is fully realized and stays | ||
| /// at zero: the continuous fee only accrues up to maturity, so there is nothing left to accrue. | ||
| /// This implies that the net credit is equal to the credit once a position has been accrued at or after maturity. | ||
| invariant pendingFeeZeroAfterMaturity(Midnight.Market market, bytes32 id, address user) | ||
| toId(market) == id && lastAccrual(id, user) >= market.maturity => pendingFee(id, user) == 0; | ||
|
|
||
| /// RULES /// | ||
|
|
||
| /// The up-to-date face value of a lender's position (credit - pendingFee) can only change by | ||
| /// withdrawing, taking, or liquidating. Every other function leaves it unchanged. | ||
| rule netCreditUnaffected(env e, method f, calldataarg args, Midnight.Market market, address user) | ||
| filtered { | ||
| f -> !f.isView | ||
| && f.selector != sig:withdraw(Midnight.Market, uint256, address, address).selector | ||
| && f.selector != sig:take(Midnight.Offer, bytes, uint256, address, address, address, bytes).selector | ||
| && f.selector != sig:liquidate(Midnight.Market, uint256, uint256, uint256, address, bool, address, address, bytes).selector | ||
|
QGarchery marked this conversation as resolved.
|
||
| } { | ||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| f(e, args); | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter == creditBefore; | ||
| } | ||
|
|
||
| /// Withdrawing on behalf of another account does not change an unrelated user's net credit. | ||
| rule withdrawDoesNotChangeOtherNetCredit(env e, Midnight.Market withdrawMarket, uint256 units, address onBehalf, address receiver, Midnight.Market market, address user) { | ||
| require user != onBehalf || toId(withdrawMarket) != toId(market), "withdrawing for someone else or on another market"; | ||
|
|
||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| withdraw(e, withdrawMarket, units, onBehalf, receiver); | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter == creditBefore; | ||
| } | ||
|
|
||
| /// Withdrawing cannot increase net credit. | ||
| rule withdrawNetCreditNonIncreasing(env e, Midnight.Market withdrawMarket, uint256 units, address onBehalf, address receiver, Midnight.Market market, address user) { | ||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| withdraw(e, withdrawMarket, units, onBehalf, receiver); | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter <= creditBefore; | ||
| } | ||
|
|
||
| /// Taking does not change the net credit of a user that is neither the taker nor the offer's maker. | ||
| rule takeDoesNotChangeOtherNetCredit(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData, Midnight.Market market, address user) { | ||
| require (user != taker && user != offer.maker) || toId(offer.market) != toId(market), "user is not involved in the take or another market"; | ||
|
|
||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter == creditBefore; | ||
| } | ||
|
|
||
| /// Taking does not decrease the net credit of a buyer in a take, and does not increase the net credit of a seller in a take. | ||
| rule takeNetCreditChangeForBuyerAndSeller(env e, Midnight.Offer offer, bytes ratifierData, uint256 units, address taker, address receiver, address takerCallback, bytes takerCallbackData, address user) { | ||
| mathint creditBefore = netCredit(e, offer.market, user); | ||
|
|
||
| take(e, offer, ratifierData, units, taker, receiver, takerCallback, takerCallbackData); | ||
|
|
||
| // We require it after `take`, because `take` may initialize the market first. | ||
| require continuousFee(toId(offer.market)) <= MAX_CONTINUOUS_FEE(), "See continuousFeeBounded in Midnight.sol"; | ||
| require offer.market.maturity <= e.block.timestamp + MAX_TTM(), "Maturity not too far in the future"; | ||
| assert continuousFee(toId(offer.market)) * zeroFloorSub(offer.market.maturity, e.block.timestamp) <= WAD(), "interest <= 100%"; | ||
|
|
||
| mathint creditAfter = netCredit(e, offer.market, user); | ||
|
|
||
| address buyer = offer.buy ? offer.maker : taker; | ||
| address seller = offer.buy ? taker : offer.maker; | ||
| assert user == buyer => creditAfter >= creditBefore; | ||
| assert user == seller => creditAfter <= creditBefore; | ||
| } | ||
|
|
||
| /// Liquidating does not change any user's net credit as long as no bad debt is realized on the same market, | ||
| /// i.e. the market loss factor is unchanged by the liquidation. | ||
| rule liquidateWithoutBadDebtDoesNotChangeCredit(env e, Midnight.Market liquidateMarket, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, bool postMaturityMode, address receiver, address callback, bytes data, Midnight.Market market, address user) { | ||
| bytes32 id = toId(market); | ||
| uint128 lossFactorBefore = lossFactor(id); | ||
|
|
||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| liquidate(e, liquidateMarket, collateralIndex, seizedAssets, repaidUnits, borrower, postMaturityMode, receiver, callback, data); | ||
|
|
||
| // Restrict to executions in which no bad debt was realized or that are on other markets. | ||
| require lossFactor(id) == lossFactorBefore || toId(liquidateMarket) != toId(market), "no bad debt realized or on different market"; | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter == creditBefore; | ||
| } | ||
|
|
||
| /// Liquidation cannot increase net credit. | ||
| rule liquidateNetCreditNonIncreasing(env e, Midnight.Market liquidateMarket, uint256 collateralIndex, uint256 seizedAssets, uint256 repaidUnits, address borrower, bool postMaturityMode, address receiver, address callback, bytes data, Midnight.Market market, address user) { | ||
| mathint creditBefore = netCredit(e, market, user); | ||
|
|
||
| require forall uint256 a. forall uint256 b1. forall uint256 b2. forall uint256 d. d > 0 => b1 <= b2 => ghostMulDivUp(a, b1, d) <= ghostMulDivUp(a, b2, d), "See mulDivMonotoneB in MulDiv.spec"; | ||
| require forall uint256 a. forall uint256 b1. forall uint256 b2. forall uint256 d. d > 0 => a <= d => b2 <= b1 => ghostMulDivUp(a, b1, d) - ghostMulDivUp(a, b2, d) <= b1 - b2, "See mulDivLipschitzB in MulDiv.spec"; | ||
| require forall uint256 a. forall uint256 b. forall uint256 d. d > 0 => b <= d => ghostMulDivUp(a, b, d) <= a, "See mulDivArgumentLesserThanDenominator in MulDiv.spec"; | ||
|
|
||
| require forall uint256 a. forall uint256 b1. forall uint256 b2. forall uint256 d. d > 0 => b1 <= b2 => ghostMulDivDown(a, b1, d) <= ghostMulDivDown(a, b2, d), "See mulDivMonotoneB in MulDiv.spec"; | ||
| require forall uint256 a1. forall uint256 a2. forall uint256 b. forall uint256 d. d > 0 => a1 <= a2 => ghostMulDivDown(a1, b, d) <= ghostMulDivDown(a2, b, d), "See mulDivMonotoneA in MulDiv.spec"; | ||
| require forall uint256 a1. forall uint256 a2. forall uint256 b. forall uint256 d. d > 0 => b <= d => a1 <= a2 => ghostMulDivDown(a2, b, d) - ghostMulDivDown(a1, b, d) <= a2 - a1, "See mulDivLipschitzA in MulDiv.spec"; | ||
|
|
||
| liquidate(e, liquidateMarket, collateralIndex, seizedAssets, repaidUnits, borrower, postMaturityMode, receiver, callback, data); | ||
|
|
||
| mathint creditAfter = netCredit(e, market, user); | ||
|
|
||
| assert creditAfter <= creditBefore; | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.