Ground Truth (verified)
The single source of truth for every MUSD contract fact musd-kit depends on. If a contract fact is not in this file, it is not yet verified, STOP and verify it before relying on it.
Verification provenance: all facts below verified on 14 June 2026 against:
- the MUSD contracts at
github.com/mezo-org/musd(main), Solidity source + interfaces, - the committed deployment artifacts at
solidity/artifacts/deployments/{mainnet,matsnet}(addresses), docs/simpleInterest.mdin that repo (the interest model),- the live
@mezo-org/passport@0.17.2package on the npm registry (peer ranges), - the Mezo developer/user docs at
mezo.org/docsand the public chain registries (chain IDs, token addresses).
Re-verification policy: before a public release, confirm the addresses below on the Mezo block explorer ("Read as Proxy"); these are proxy contracts and a future upgrade would change implementation behavior behind the same proxy. The proxy addresses are stable; treat them as bundled-per-release with an override (O5).
1. Networks
| Network | EVM chainId | Cosmos chain-id | Native gas | Notes |
|---|---|---|---|---|
| Mezo Mainnet | 31612 | mezo_31612-1 | BTC (18 decimals) | repo deployment dir mainnet/ |
| Mezo Testnet | 31611 | n/a | BTC (18 decimals) | repo deployment dir matsnet/ ("matsnet" = Mezo testnet) |
Native gas is BTC with 18 decimals (not 8). Collateral sent to openTrove / addColl is msg.value in BTC wei (1e18).
2. Fixed constants (bundled, no on-chain setter exists)
From dependencies/LiquityBase.sol and dependencies/BaseMath.sol:
_100pct = 1e18 // 100%
MCR = 1.1e18 // 110%, individual liquidation trigger (ICR < MCR)
CCR = 1.5e18 // 150%, Recovery Mode threshold (system TCR < CCR)
MUSD_GAS_COMPENSATION = 200e18 // 200 MUSD, reserve added at open, returned on close
PERCENT_DIVISOR = 200 // liquidator collateral reward = coll / 200 = 0.5%
DECIMAL_PRECISION = 1e18
MIN_NET_DEBT_MIN = 50e18 // hard floor: governance cannot set minNetDebt below thisThese may be bundled as constants in the SDK. Everything in §3 may not.
3. Governable / dynamic values (MUST be read on-chain)
| Value | How to read | Init / current | Governance path |
|---|---|---|---|
| Minimum net debt | borrowerOperations.minNetDebt() | 1800e18 | proposeMinNetDebt → approveMinNetDebt (≥ MIN_NET_DEBT_MIN) |
| Borrowing fee | borrowerOperations.getBorrowingFee(uint256 _debt) view | flat 0.1% (10 bps) of the draw, NO floor/cap (verified Phase 4: fee(d) = d/1000 exactly at 1.8k-1M MUSD), still governable, so READ it | proposeBorrowingRate → approveBorrowingRate |
| Redemption rate | borrowerOperations.redemptionRate(), governable; applied to ALL redeemers (⚠ the "0% if redeemer holds a loan" rule was disproven on the fork in Phase 6, see §8) | ~0.75% | proposeRedemptionRate → approveRedemptionRate |
| Global interest rate | interestRateManager.interestRate() → uint16 (bips) | 1 to 5% band | proposeInterestRate(uint16) → approveInterestRate |
| Oracle price (BTC/USD) | priceFeed.fetchPrice() view → uint (1e18-scaled) | live | n/a (oracle) |
Cache these per createMusdClient session; refresh on demand.
Oracle is a native precompile (verified 14 Jun 2026, Phase 0).
priceFeed.fetchPrice()staticcalls a Chainlink-style aggregator at the native precompile0x7b7c000000000000000000000000000000000015(decimals()+latestRoundData(),decimals == 18). On the real Mezo node that address is served by a Cosmos oracle module; its stored EVM bytecode is only a fallback that self-recurses (its deeper target0x15 + (0x1edf << 0x92)resolves back to the same address). Consequence for tooling: a plain anvil EVM fork copies the bytecode but has no native handler, sofetchPrice()reverts on a fork even though pure-EVM reads (MCR, Trove storage) work. The test harness shims this one external precompile with the real live round data, never a MUSD contract. See07-testing.md§1 andpackages/core/test/harness/README.md.
4. Contract address maps (verified from deployment artifacts)
Sourced from solidity/artifacts/deployments/{mainnet,matsnet} at main. Cross-check: the MUSD token addresses here match the Mezo docs exactly, which validates the set.
Official sourcing (verified on npm, 14 Jun 2026), prefer these over hand-bundling:
- ABIs + addresses:
@mezo-org/musd-contracts(v1.1.0), the official package of "MUSD smart contract deployment artifacts … addresses and ABIs generated by Hardhat."musd-kitshould source its ABIs and address maps from this package (versioned, official) rather than hand-transcribing them, and pin its version. The tables below remain the human-readable, verified reference and the fallback override. - Chain config:
@mezo-org/chains, the official viem chain config (mezoMainnet,mezoTestnet,createMezoChain).musd-kitshould depend on this for chain definitions instead of hand-rolling them. (Note: this is a correction to the v0.1 framing, there are official npm artifacts/config packages; what remains absent is a typed interaction client, which is exactlymusd-kit.)
4.1 Mezo Mainnet (chainId 31612)
| Contract | Address |
|---|---|
| BorrowerOperations | 0x44b1bac67dDA612a41a58AAf779143B181dEe031 |
| TroveManager | 0x94AfB503dBca74aC3E4929BACEeDfCe19B93c193 |
| SortedTroves | 0x8C5DB4C62BF29c1C4564390d10c20a47E0b2749f |
| HintHelpers | 0xD267b3bE2514375A075fd03C3D9CBa6b95317DC3 |
| PriceFeed | 0xc5aC5A8892230E0A3e1c473881A2de7353fFcA88 |
| InterestRateManager | 0x4a453700d157717Fe02fB62E7700ED7845048285 |
| MUSD (token) | 0xdD468A1DDc392dcdbEf6db6e34E89AA338F9F186 |
| StabilityPool | 0x73245Eff485aB3AAc1158B3c4d8f4b23797B0e32 |
| ActivePool | 0x3012C2fE1240e3754E5C200A0946bb0E07474876 |
| DefaultPool | 0xE4B5913C0c82dB2eFC553b95c0173efb90a07c8B |
| CollSurplusPool | 0xBF51807ACb3394B8550f0554FB9098856Ef5F491 |
| GasPool | 0x3EB418BdBE95b4b9cf465ecfBD8424685ACD1Bc1 |
| PCV | 0x391EcC7ffEFc48cff41D0F2Bb36e38b82180B993 |
| BorrowerOperationsSignatures | 0xB57ab578BF20b3e318f3EFAA587C51DBccE5df7a |
4.2 Mezo Testnet (chainId 31611)
| Contract | Address |
|---|---|
| BorrowerOperations | 0xCdF7028ceAB81fA0C6971208e83fa7872994beE5 |
| TroveManager | 0xE47c80e8c23f6B4A1aE41c34837a0599D5D16bb0 |
| SortedTroves | 0x722E4D24FD6Ff8b0AC679450F3D91294607268fA |
| HintHelpers | 0x4e4cBA3779d56386ED43631b4dCD6d8EacEcBCF6 |
| PriceFeed | 0x86bCF0841622a5dAC14A313a15f96A95421b9366 |
| InterestRateManager | 0xD4D6c36A592A2c5e86035A6bca1d57747a567f37 |
| MUSD (token) | 0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503 |
| StabilityPool | 0x1CCA7E410eE41739792eA0A24e00349Dd247680e |
| ActivePool | 0x143A063F62340DA3A8bEA1C5642d18C6D0F7FF51 |
| DefaultPool | 0x59851D252090283f9367c159f0C9036e75483300 |
| CollSurplusPool | 0xB4C35747c26E4aB5F1a7CdC7E875B5946eFa6fa9 |
| GasPool | 0x8fa3EF45137C3AFF337e42f98023C1D7dd3666C0 |
| PCV | 0x4dDD70f4C603b6089c07875Be02fEdFD626b80Af |
| BorrowerOperationsSignatures | 0xD757e3646AF370b15f32EB557F0F8380Df7D639e |
The SDK wraps the developer-facing subset (BorrowerOperations, TroveManager, SortedTroves, HintHelpers, PriceFeed, InterestRateManager, MUSD token). The pools, PCV, and StabilityPool are protocol-internal; bundle their addresses for completeness and read-through where needed. ABIs are taken from the repo interfaces / artifacts and bundled, typed, per package.
Multicall3 (verified on the fork, Phase 2): deployed on Mezo at the canonical cross-chain address 0xcA11bde05977b3631167028862bE2a173976CA11 (code present, getBlockNumber() answers). musd-kit uses it to batch live reads into one same-block snapshot. Note @mezo-org/chains does not declare contracts.multicall3, so the SDK passes this address to viem multicall explicitly.
No MUSD/USD peg oracle: PriceFeed exposes only fetchPrice() (BTC/USD) + oracle(); there is no MUSD/USD price source on-chain. getPeg() is therefore not implemented (it would require guessing a peg).
5. The ABI surface musd-kit wraps
5.1 IBorrowerOperations (Trove lifecycle, writes + a fee read)
// FULL SIGNATURES, verified from the artifacts (Phase 5). NO maxFeePercentage on ANY
// of these (C5 extends beyond openTrove). Hints are (_upperHint, _lowerHint).
openTrove(uint256 _debtAmount, address _upperHint, address _lowerHint) payable
addColl(address _upperHint, address _lowerHint) payable // collateral via msg.value
withdrawColl(uint256 _amount, address _upperHint, address _lowerHint) // collateral out
withdrawMUSD(uint256 _amount, address _upperHint, address _lowerHint) // borrow more (mint)
repayMUSD(uint256 _amount, address _upperHint, address _lowerHint) // reduce debt
adjustTrove(uint256 _collWithdrawal, uint256 _debtChange, bool _isDebtIncrease,
address _upperHint, address _lowerHint) payable // add-coll via msg.value
closeTrove()
refinance(address _upperHint, address _lowerHint)
claimCollateral()
getBorrowingFee(uint256 _debt) view returns (uint)
refinancingFeePercentage() view returns (uint) // governable refinance feeNote both addColl/withdrawMUSD (single-axis) and adjustTrove (combined) exist; route single-axis SDK intents to the dedicated functions, combined ones to adjustTrove.
Write mechanics verified on the fork (Phase 5):
- No ERC-20 approval needed for
repayMUSD/closeTrove. BorrowerOperations has protocol burn authority and burns MUSD directly from the caller,repayMUSDsucceeds with allowance 0. The SDK sends noapprove. closeTrovepayoff: the caller must holdentireDebt − 200MUSD (the net debt; the 200 gas reserve is burned from the GasPool, not the caller). On close the 200 reserve + the collateral are returned to the caller, andgetTroveStatusbecomes 2 (closedByOwner) →getTrove.exists == false.refinanceadds a refinancing fee to the debt (refinancingFeePercentage, governable) and moves the Trove to the current global rate (unchanged if already at it). Hints recomputed from the current position are "good enough" (placement is contract-guaranteed; hints only affect gas).
5.2 IHintHelpers (insertion + redemption hints; pure CR helpers)
getApproxHint(uint256 _CR, uint256 _numTrials, uint256 _inputRandomSeed)
view returns (address hintAddress, uint256 diff, uint256 latestRandomSeed)
getRedemptionHints(uint256 _amount, uint256 _price, uint256 _maxIterations)
view returns (address firstRedemptionHint, uint256 partialRedemptionHintNICR, uint256 truncatedAmount)
computeNominalCR(uint256 _coll, uint256 _debt) pure returns (uint)
computeCR(uint256 _coll, uint256 _debt, uint256 _price) pure returns (uint)5.3 ITroveManager (authoritative reads + permissionless writes)
// READS, use these for LIVE position data
// VERIFIED SHAPE (fork, Phase 2): 6 fields, computed TO NOW.
getEntireDebtAndColl(address _borrower) view returns (
uint256 coll, uint256 principal, uint256 interest,
uint256 pendingCollateral, uint256 pendingPrincipal, uint256 pendingInterest)
// → entireDebt = principal + interest (== the debt getCurrentICR uses; proven via computeCR).
getCurrentICR(address _borrower, uint256 _price) view returns (uint)
getNominalICR(address _borrower) view returns (uint)
getTroveInterestOwed(address _borrower) view returns (uint) // ⚠ STORED (STALE) snapshot, see §7
getTroveDebt(address _borrower) view returns (uint) // ⚠ STORED principal (incl gas+fee), no live interest
getTroveColl(address _borrower) view returns (uint)
getTroveStatus(address _borrower) view returns (uint8 Status) // enum: 0 nonExistent, 1 active (verified);
// 2 closedByOwner, 3 closedByLiquidation,
// 4 closedByRedemption (source order)
getTroveInterestRate(address _borrower) view returns (uint16) // basis points
getTCR(uint256 _price) view returns (uint)
checkRecoveryMode(uint256 _price) view returns (bool)
// WRITES, permissionless (keeper surface)
redeemCollateral(...)
liquidate(address _borrower)
batchLiquidateTroves(address[] _troveArray)5.4 IInterestRateManager
interestRate() view returns (uint16) // current GLOBAL rate, in bips
getAccruedInterest() view returns (uint256) // system-wide accrued interest
// governed via proposeInterestRate(uint16) / approveInterestRate5.5 IPriceFeed
fetchPrice() view returns (uint) // BTC/USD, 1e18-scaled. In MUSD this is view (callable in read context).5.6 ISortedTroves (used by the hint module)
findInsertPosition(uint256 _NICR, address _prevId, address _nextId) view returns (address upperHint, address lowerHint), called with the approx hint from getApproxHint as both _prevId and _nextId. Also exposes getSize() → uint256, getFirst()/getLast(), getPrev(id)/getNext(id), contains(id) → bool, and validInsertPosition(_NICR, _prevId, _nextId) → bool.
Ordering = DESCENDING NICR (verified on the fork, Phase 3). getFirst() is the highest NICR, getLast() the lowest; getNext(id) walks toward lower NICR (the tail), getPrev(id) toward higher (the head). So for an insert position, upperHint (prev) has NICR ≥ the new NICR, lowerHint (next) has NICR ≤ it.NICR = computeNominalCR(coll, debt) = coll · 1e20 / debt (NICR_PRECISION = 1e20, verified exact). Insertion-hint trial count uses the Liquity heuristic numTrials = ceil(15·√getSize()), clamped to [15, 2500].
5.7 MUSD token
ERC-20 at the addresses in §4. Standard balanceOf, allowance, approve, transfer. 18 decimals.
6. The exact open-trove debt math (verified from BorrowerOperations.sol)
// User calls openTrove(_debtAmount, upperHint, lowerHint), msg.value = collateral (BTC wei)
fee = getBorrowingFee(_debtAmount) // minted to PCV, ADDED to the borrower's debt
netDebt = _debtAmount + fee // borrower still RECEIVES _debtAmount in MUSD
require(netDebt >= minNetDebt) // floor on (draw + fee); currently 1,800 MUSD; governable
entireDebt = netDebt + MUSD_GAS_COMPENSATION // + 200 ("composite" / entire debt)
ICR = computeCR(msg.value, entireDebt, price)// = (collateral * price) / entireDebtDefinitions used throughout the SDK:
_getCompositeDebt(debt) = debt + 200_getNetDebt(debt) = debt − 200- On a debt increase (
adjustTrove/withdrawMUSD):netDebtChange += getBorrowingFee(change), the fee is added to the debt change too. - On close: payoff =
entireDebt; the 200 gas reserve is returned.
For previewOpen / getBorrowingPower:
- entire debt at open =
draw + getBorrowingFee(draw) + 200 - minimum valid draw satisfies
draw + getBorrowingFee(draw) ≥ minNetDebt - borrowing power = largest
drawsuch that(collateral × price) / (draw + fee(draw) + 200) ≥ MCR, subject to the net floor, and with the rate fixed at the 110%-CR max capacity (see §7).
7. The interest model (verified from docs/simpleInterest.md)
- Simple (non-compounding), time-based, linear. Interest accrues by elapsed seconds, not blocks:
new_interest = interest_numerator × (current_timestamp − last_update) / seconds_per_year. SECONDS_PER_YEAR = 31_556_952(= 365.2425 days × 86400, the Gregorian year, NOT 365 (31_536_000) nor 365.25 (31_557_600)). Verified on the fork (Phase 4): open, warp a known elapsed, readgetEntireDebtAndColl.interest, back out the constant; the forward predictioninterest = principal · rateBips · elapsed / (10_000 · 31_556_952)matches to the wei.- Interest base = the full stored principal (
draw + fee + 200gas comp), not the net draw, verified (a 5,000-draw Trove accrues onprincipal = 5,205). - Rates are in basis points (
interestRate()isuint16). - Per-Trove interest updates only on interaction with that Trove → the stored
interestOwedis stale between interactions. For live entire-debt, readgetEntireDebtAndColl, which computes to the current time. Never read the stored value and call it current.- CORRECTION (verified on the fork, Phase 2):
getTroveInterestOwedandgetTroveDebtreturn the STORED (stale) snapshot, they do NOT compute to now. After a 30-day clock warp,getEntireDebtAndColl.interestgrew to ~1.81 MUSD whilegetTroveInterestOwedstayed0. So the to-now interest isgetEntireDebtAndColl.interest, and liveentireDebt = principal + interestfrom that one getter.musd-kit'sgetTrovesourcesinterestOwed/principalfromgetEntireDebtAndColl, never from the stored getters. (Earlier drafts of this section lumpedgetTroveInterestOwedin with the to-now getter, it is not.)
- CORRECTION (verified on the fork, Phase 2):
- A Trove's fixed rate is set at open from its maximum borrowing capacity at 110% CR, not from the initial draw.
refinancemoves a Trove to the current global rate.
8. Protocol mechanics (verified)
- Liquidation is permissionless (anyone):
liquidate(address _borrower),batchLiquidateTroves(address[] _troveArray). Liquidator reward (verified on the fork, Phase 6, via theLiquidationevent): 200 MUSD (_gasCompensation, paid to the caller's MUSD balance) + 0.5% of the Trove's BTC (_collGasCompensation= coll/PERCENT_DIVISOR). On liquidation the Trove's status → 3 (closedByLiquidation). - Liquidatability (refines the Phase-2 normal-mode-only note): normal mode (TCR ≥ CCR) → liquidatable iff
ICR < MCR; Recovery Mode (TCR < CCR) → liquidatable iffICR < CCR(a Trove withICR ≥ CCRis never liquidated). Verified normal mode on the fork; RM is inducible viasetPrice(TCR < CCR). Useful events:Liquidation(_liquidatedPrincipal, _liquidatedInterest, _liquidatedColl, _collGasCompensation, _gasCompensation)andTroveLiquidated(address indexed _borrower, uint256 _debt, uint256 _coll, uint8 operation). - Recovery Mode activates when system TCR < CCR (150%); borrowing rules tighten.
- Redemption (any MUSD holder): burns MUSD for BTC from the lowest-ICR Troves above 110%. Signature (verified):
redeemCollateral(uint256 _amount, address _firstRedemptionHint, address _upperPartialRedemptionHint, address _lowerPartialRedemptionHint, uint256 _partialRedemptionHintNICR, uint256 _maxIterations)with no_maxFeePercentage(C5 extends to redemption; any fee guard is SDK-side).- Ritual:
getRedemptionHints(amount, price, maxIterations) → (firstRedemptionHint, partialRedemptionHintNICR, truncatedAmount); thengetApproxHint(partialNICR) → findInsertPosition(partialNICR, approx, approx)→ pass(first, upper, lower, partialNICR, maxIterations)toredeemCollateral. - FEE, CORRECTION (verified on the fork, Phase 6): the
0%-for-loan-holdersrule does NOT hold in this deployment. A redeemer who holds an open loan paid the fullborrowerOperations.redemptionRate()= 0.75% (measured from theRedemptionevent:_collateralFee / (_collateralSent + _collateralFee) ≈ 0.744%). The SDK readsredemptionRate()and applies it to all redeemers (governable, never hardcode 0.75%). truncatedAmountis the redeemable amount given theminNetDebtfloor on the last touched Trove (andmaxIterations). TheRedemptionevent reports(_attemptedAmount, _actualAmount, _collateralSent, _collateralFee), the actual redeemed amount can be less thantruncatedAmountwhen the redemption needs a partial of the last Trove and the partial hint drifts (the partial is skipped, full Troves still redeem). Truncation that lands on full-Trove boundaries (e.g. theminNetDebtfloor) redeems exactlytruncatedAmount. Compute hints immediately before sending and do not mine a block between the hint and the redeem (interest drift invalidates the partial hint).- Surplus/claim: a fully-redeemed Trove (status → 4, closedByRedemption) and an RM-liquidated above-MCR Trove leave surplus collateral in the CollSurplusPool (
0xB4C35747…,getCollateral(address)); the borrower withdraws it viaclaimCollateral(). A liquidation atICR ≈ 1.0leaves no surplus.
- Ritual:
9. The corrections (what looks one way but is another)
These are the verified deltas from stock-Liquity expectations. Internalize them; they are the "everyone gets it wrong" cases.
| # | The trap | The truth |
|---|---|---|
| C1 | "Min debt is 2,000, fixed" | 1,800 net, governable (minNetDebt()); 2,000 = net + 200 gas |
| C2 | "Fees are constants (0.1% / 0.75%)" | Governable; read getBorrowingFee + redemption rate on-chain |
| C3 | "Interest accrues per block" | Per elapsed second, linear, bips; stored value is stale → read getters |
| C4 | "Rate is set from the initial draw" | Set from the 110%-CR max capacity at open |
| C5 | "openTrove takes a maxFeePercentage" | It does not; any fee guard is SDK-side |
| C6 | "Borrowing power floor is 2,000 net" | Floor is (draw + fee) ≥ minNetDebt; entire debt = draw + fee + 200 |
| C7 | "Function names are repay/adjust/withdraw" | ABI names are repayMUSD/withdrawColl/withdrawMUSD/addColl/… |
| C8 | "We must re-derive the math" | True only for previews; live reads use contract getters |
10. Still-open items (do not depend on these)
- F1 / O6, the "vault".
IMUSDSavingsRateexposes onlyreceiveProtocolYield(uint256)(a protocol-side yield receiver), not a user deposit/withdraw vault. The v2 "vault helpers" premise is unverified. New color (14 Jun 2026): Mezo does have live "vaults" advertising triple-digit APRs, but these appear to be liquidity-provider / Earn-side venues (Tigris/Aerodrome LP, veBTC), not a simple idle-MUSD savings vault;@metalend/sdkalso offers gasless MUSD deposits as its own protocol. Identify the exact MUSD-deposit venue + its contract before committing v2 vault scope. - F3, emptiness re-check. RESOLVED (14 Jun 2026). An exhaustive npm + GitHub scan found no neutral, typed MUSD CDP client SDK. The closest packages are not competitors:
payce-musd-sdk(a micropayment product's SDK that wraps MUSD borrowing internally),mezo-compose-sdk(a portfolio/allocation/swap SDK on Tigris DEX, different layer), and@gitmyabi-stg/musd(raw auto-generated type bindings). AppspikoloandMezo-Defi-IQhand-build the Trove layer inside their own products. The "wagmi+RainbowKit-of-MUSD / Liquity-lib-react-for-Mezo" niche is unoccupied.
11. Revert reasons (the real corpus, triggered on the fork, Phase 7)
Form (verified): every MUSD protocol-logic revert reachable from the SDK surface is a classic Liquity require-string (Error(string)), not a Solidity custom error. The only custom errors in the trove/manager ABIs are OpenZeppelin boilerplate (OwnableUnauthorizedAccount, InvalidInitialization, …) which the SDK never triggers; the MUSD token carries OZ ERC-20 custom errors but BorrowerOperations' own require fires first (see InsufficientMusdBalance below). One exception: repaying more than owed is a Panic(0x11) (arithmetic underflow), not a string.
Each row was produced by triggering the scenario against the forked contracts and capturing viem's decoded reason (via ContractFunctionRevertedError):
| Scenario (how triggered) | Exact revert | Form | → typed error |
|---|---|---|---|
openTrove with draw+fee < minNetDebt | BorrowerOps: Trove's net debt must be greater than minimum | require | BelowMinimumDebt |
openTrove/withdrawColl/adjustTrove leaving ICR < MCR (normal mode) | BorrowerOps: An operation that would result in ICR < MCR is not permitted | require | ICRBelowMCR |
openTrove with MCR ≤ ICR < CCR while system in Recovery Mode | BorrowerOps: Operation must leave trove with ICR >= CCR | require | RecoveryModeRestriction |
repayMUSD more than owed | Arithmetic operation resulted in underflow or overflow. (Panic 0x11) | panic | RepayExceedsDebt |
repayMUSD with insufficient MUSD held | BorrowerOps: Caller doesnt have enough mUSD to make repayment | require | InsufficientMusdBalance |
op on an address with no Trove (withdrawColl/closeTrove/…) | BorrowerOps: Trove does not exist or is closed | require | TroveNotFound |
openTrove when one is already open | BorrowerOps: Trove is active | require | TroveAlreadyExists |
liquidate a healthy Trove | TroveManager: nothing to liquidate | require | NothingToLiquidate |
redeemCollateral that can redeem nothing (incl. a stale/bad partial hint) | TroveManager: Unable to redeem any amount | require | RedemptionFailed |
Not reachable from the SDK surface (marked, not invented):
StaleHint, there is no distinct "stale hint" revert. A stale/incorrect redemption partial hint does not produce its own error; it makes the redemption redeem nothing →TroveManager: Unable to redeem any amount(→RedemptionFailed). Insertion hints never revert (open/adjust re-traverse from a bad hint).StaleHintremains a defined, exported error (public API since Phase 6) but is documented as not distinctly reachable; redemption-hint staleness surfaces asRedemptionFailed.Unauthorized, the SDK surface calls no governance/permission-gated function, so theOwnableUnauthorizedAccountpath is unreachable. Defined/exported for completeness only.InsufficientCollateral, the on-chainICR < MCRrevert maps toICRBelowMCR(contract-authoritative).InsufficientCollateralis retained as the preview-time sibling (for the math/React layer); the write path surfacesICRBelowMCR.
Decoder discipline: mapRevert matches on the distinctive substring of each reason (case-insensitive), maps the repay Panic via operation context, and sends anything unrecognized to ContractCallFailed with the raw viem error preserved in cause.