Skip to content

Math & Hints (the correctness-critical core)

This is the heart of the library. The math here is the only client-side computation in musd-kit, and it is held to the highest test bar (07-testing). Everything is verified against 01-ground-truth §6-§7; this document is the implementation-level spec.

The boundary, restated: for a live position, do not use anything here, read the contract getters (read/, 03-core-api §2). The formulas below are for previews of positions that do not exist yet, and for the thin derivations (liquidationPrice, healthFactor) layered on authoritative live values.

All quantities are bigint in 1e18 fixed point unless noted. Rates are basis points (uint16 on-chain).


1. The verified quantities

QuantityFormulaNotes
Borrowing feefee = getBorrowingFee(draw)read on-chain; governable (C2)
Net debtnetDebt = draw + feethe fee is added to debt (C6)
Entire (composite) debtentireDebt = netDebt + 200e18+ gas compensation
ICRicr = (collateral × price) / entireDebt1e18 fixed point; = contract computeCR
Nominal ICR (for hints)nicr = (collateral × 1e20) / entireDebt= contract computeNominalCR (1e20 = 100·1e18)
Liquidation priceliqPrice = (MCR × entireDebt) / collateralMCR = 1.1e18; price at which ICR == MCR
Min-debt checknetDebt ≥ minNetDebtread minNetDebt(); floor on draw+fee (C1, C6)
Health factornormalize icr / MCR → distance-to-liquidation1.0 at MCR, higher = safer (UI/keeper)

1e20 for NICR is exact: nominal CR uses collateral × 100e18 / entireDebt, and 100e18 == 1e20. Cross-check the SDK's NICR against HintHelpers.computeNominalCR on the fork; they must be identical.


2. Entire-debt for a PREVIEW (no live position)

A live Trove's entire debt is read from getEntireDebtAndColl, never computed. For a preview (e.g. "what will my debt be in 30 days?"), use the verified interest model (01-ground-truth §7):

interest_accrued = principal × rate_bips / 10_000 × elapsedSeconds / SECONDS_PER_YEAR   // linear, simple
entireDebt_preview = principal + interest_accrued + fees + 200e18
  • Linear, non-compounding, time-based (seconds), not blocks (C3).
  • SECONDS_PER_YEAR must match the contract's constant exactly (verify on the fork; do not assume 31_536_000 vs 31_557_600, read/confirm what the contract uses).
  • Subtlety (C4): a Trove's fixed rate is set at open from its 110%-CR maximum borrowing capacity, not the initial draw. previewOpen must compute the rate the way the contract will, then validate that the predicted rate matches the rate the contract assigns when the position is actually opened on the fork.

This preview accrual is the single most error-prone formula in the library; its fork validation (§5) is the M1 correctness gate.


3. Borrowing power

The maximum MUSD a user can draw against collateral at price, staying above MCR:

find the largest `draw` such that:
    (collateral × price) / (draw + getBorrowingFee(draw) + 200e18) ≥ MCR
  AND
    (draw + getBorrowingFee(draw)) ≥ minNetDebt      // read minNetDebt()

Because the fee depends on draw (and may be a percentage), solve for draw analytically if the fee is linear, or by monotonic search otherwise, then verify the boundary on the fork by actually opening a Trove at the computed max and confirming its ICR is ≥ MCR (and that one wei more would breach it). In Recovery Mode (TCR < CCR), the effective constraint tightens, surface it (O3) and reflect it in the returned power.

Do not use a "≥ 2,000 net" floor, the floor is minNetDebt (currently 1,800), read on-chain (C1/C6).


4. Live-position derivations (thin, over authoritative values)

For getTrove on a live position, icr, entireDebt, interestOwed, nominalICR come from contract getters. Only two fields are derived, and they are derived from those authoritative values, not recomputed from scratch:

liquidationPrice = (MCR × entireDebt_fromContract) / collateral_fromContract
healthFactor     = f(icr_fromContract / MCR)        // monotonic; 1.0 at MCR

liquidationPrice rises over time as interest accrues (entire debt grows) unless the user repays, a subtlety a naive static calculation misses. Because entireDebt here is read live (accrued to now), the derived liquidationPrice is correct at read time.


5. The dual-validation method (how preview math earns trust)

Every formula in math/ is validated twice. This is the mechanism behind "correctness is the product."

  1. Against forked-Mezo behavior (the primary gate). For a grid of inputs: compute the preview, then actually perform the operation on the fork, then read the contract's getter and assert equality to the wei (or within a documented, justified tolerance for any rounding the contract itself does).

    • previewOpen(x)openTrove(x) on the fork → getEntireDebtAndColl / getCurrentICR must match previewOpen's entireDebt / icr.
    • getBorrowingPower → open at the boundary → ICR ≥ MCR, and boundary+1 wei breaches (or the open reverts).
    • preview interest accrual → warp the fork clock → compare to getTroveInterestOwed.
  2. Against the contract's pure helpers (the cheap cross-check). Where the contract exposes the same computation as a pure function, call it and assert the SDK matches:

    • SDK computeICR vs HintHelpers.computeCR(coll, entireDebt, price)
    • SDK NICR vs HintHelpers.computeNominalCR(coll, entireDebt)
    • SDK fee usage vs getBorrowingFee(draw)

If (1) and (2) ever disagree, the contract is right and the SDK is wrong, fix the SDK, add the failing case to the boundary corpus (07-testing).


6. The hint module (hints/), the insertion-hint ritual

MUSD is a Liquity-fork CDP: every Trove lives in a list sorted by nominal CR, and opening/adjusting one requires supplying correct insertion hints (upperHint, lowerHint) for gas efficiency. The SDK wraps this once.

6.1 Insertion hints (open / adjust / withdrawColl / refinance)

1. Determine entireDebt for the resulting position (draw + fee + 200), see §1.
2. nicr = computeNominalCR(collateral, entireDebt)          // contract pure, or the §1 formula cross-checked
3. (approxHint ) = HintHelpers.getApproxHint(nicr, numTrials, randomSeed)
4. (upperHint, lowerHint) = SortedTroves.findInsertPosition(nicr, approxHint, approxHint)
5. → pass upperHint, lowerHint to the write call
  • Defaults (O2): start numTrials ≈ 15, randomSeed ≈ 42 (Mezo's example values); expose both as overridable options; document the gas/accuracy trade-off. Tune numTrials empirically against the fork (higher = better placement, more gas).
  • Re-run on every position-changing write, adjustTrove, withdrawColl, withdrawMUSD (borrow more), and refinance each change the position's place in the list, so each recomputes hints.
  • Validation: randomized (collateral, draw) pairs must produce a successful openTrove on the fork with the Trove landing in the correct sorted position and gas within tolerance of optimal.

6.2 Redemption hints (redemption/)

(firstRedemptionHint, partialRedemptionHintNICR, truncatedAmount)
    = HintHelpers.getRedemptionHints(amount, price, maxIterations)
→ pass these to TroveManager.redeemCollateral(...)
  • truncatedAmount is the amount actually redeemable given the minNetDebt floor on the last touched Trove, surface it to the caller (they may redeem less than requested).
  • Hints go stale if someone redeems before the tx lands → compute immediately before sending; document the stale-hint revert path and a retry.
  • Fee: the current redemptionRate() (read on-chain, C2), applied to ALL redeemers including loan holders. (The "0% for loan holders" rule was disproven on the fork in Phase 6, see 01-ground-truth.md §8.)

7. Invariants the tests assert (non-exhaustive)

  • For any opened position: getCurrentICR(addr, price) == computeCR(coll, getEntireDebtAndColl.debt, price).
  • previewOpen(x).entireDebt == getEntireDebtAndColl(addr).debt after opening x.
  • previewOpen(x).meetsMinimum == falseopenTrove(x) reverts BelowMinimumDebt.
  • liquidationPrice such that computeCR(coll, entireDebt, liquidationPrice) == MCR.
  • NICR from the SDK == computeNominalCR(coll, entireDebt) exactly.
  • A position with ICR just below MCR is isLiquidatable; just above is not, and a liquidation attempt reverts.

Community tooling for Mezo testnet and evaluation. Not affiliated with or endorsed by Mezo or Thesis. MIT licensed.