Skip to content

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.md in that repo (the interest model),
  • the live @mezo-org/passport@0.17.2 package on the npm registry (peer ranges),
  • the Mezo developer/user docs at mezo.org/docs and 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

NetworkEVM chainIdCosmos chain-idNative gasNotes
Mezo Mainnet31612mezo_31612-1BTC (18 decimals)repo deployment dir mainnet/
Mezo Testnet31611n/aBTC (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 this

These may be bundled as constants in the SDK. Everything in §3 may not.


3. Governable / dynamic values (MUST be read on-chain)

ValueHow to readInit / currentGovernance path
Minimum net debtborrowerOperations.minNetDebt()1800e18proposeMinNetDebtapproveMinNetDebt (≥ MIN_NET_DEBT_MIN)
Borrowing feeborrowerOperations.getBorrowingFee(uint256 _debt) viewflat 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 itproposeBorrowingRateapproveBorrowingRate
Redemption rateborrowerOperations.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%proposeRedemptionRateapproveRedemptionRate
Global interest rateinterestRateManager.interestRate()uint16 (bips)1 to 5% bandproposeInterestRate(uint16)approveInterestRate
Oracle price (BTC/USD)priceFeed.fetchPrice() view → uint (1e18-scaled)liven/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 precompile 0x7b7c000000000000000000000000000000000015 (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 target 0x15 + (0x1edf << 0x92) resolves back to the same address). Consequence for tooling: a plain anvil EVM fork copies the bytecode but has no native handler, so fetchPrice() 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. See 07-testing.md §1 and packages/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-kit should 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-kit should 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 exactly musd-kit.)

4.1 Mezo Mainnet (chainId 31612)

ContractAddress
BorrowerOperations0x44b1bac67dDA612a41a58AAf779143B181dEe031
TroveManager0x94AfB503dBca74aC3E4929BACEeDfCe19B93c193
SortedTroves0x8C5DB4C62BF29c1C4564390d10c20a47E0b2749f
HintHelpers0xD267b3bE2514375A075fd03C3D9CBa6b95317DC3
PriceFeed0xc5aC5A8892230E0A3e1c473881A2de7353fFcA88
InterestRateManager0x4a453700d157717Fe02fB62E7700ED7845048285
MUSD (token)0xdD468A1DDc392dcdbEf6db6e34E89AA338F9F186
StabilityPool0x73245Eff485aB3AAc1158B3c4d8f4b23797B0e32
ActivePool0x3012C2fE1240e3754E5C200A0946bb0E07474876
DefaultPool0xE4B5913C0c82dB2eFC553b95c0173efb90a07c8B
CollSurplusPool0xBF51807ACb3394B8550f0554FB9098856Ef5F491
GasPool0x3EB418BdBE95b4b9cf465ecfBD8424685ACD1Bc1
PCV0x391EcC7ffEFc48cff41D0F2Bb36e38b82180B993
BorrowerOperationsSignatures0xB57ab578BF20b3e318f3EFAA587C51DBccE5df7a

4.2 Mezo Testnet (chainId 31611)

ContractAddress
BorrowerOperations0xCdF7028ceAB81fA0C6971208e83fa7872994beE5
TroveManager0xE47c80e8c23f6B4A1aE41c34837a0599D5D16bb0
SortedTroves0x722E4D24FD6Ff8b0AC679450F3D91294607268fA
HintHelpers0x4e4cBA3779d56386ED43631b4dCD6d8EacEcBCF6
PriceFeed0x86bCF0841622a5dAC14A313a15f96A95421b9366
InterestRateManager0xD4D6c36A592A2c5e86035A6bca1d57747a567f37
MUSD (token)0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503
StabilityPool0x1CCA7E410eE41739792eA0A24e00349Dd247680e
ActivePool0x143A063F62340DA3A8bEA1C5642d18C6D0F7FF51
DefaultPool0x59851D252090283f9367c159f0C9036e75483300
CollSurplusPool0xB4C35747c26E4aB5F1a7CdC7E875B5946eFa6fa9
GasPool0x8fa3EF45137C3AFF337e42f98023C1D7dd3666C0
PCV0x4dDD70f4C603b6089c07875Be02fEdFD626b80Af
BorrowerOperationsSignatures0xD757e3646AF370b15f32EB557F0F8380Df7D639e

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)

solidity
// 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 fee

Note 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, repayMUSD succeeds with allowance 0. The SDK sends no approve.
  • closeTrove payoff: the caller must hold entireDebt − 200 MUSD (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, and getTroveStatus becomes 2 (closedByOwner)getTrove.exists == false.
  • refinance adds 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)

solidity
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)

solidity
// 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

solidity
interestRate() view returns (uint16)         // current GLOBAL rate, in bips
getAccruedInterest() view returns (uint256)  // system-wide accrued interest
// governed via proposeInterestRate(uint16) / approveInterestRate

5.5 IPriceFeed

solidity
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) / entireDebt

Definitions 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 draw such 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, read getEntireDebtAndColl.interest, back out the constant; the forward prediction interest = principal · rateBips · elapsed / (10_000 · 31_556_952) matches to the wei.
  • Interest base = the full stored principal (draw + fee + 200 gas comp), not the net draw, verified (a 5,000-draw Trove accrues on principal = 5,205).
  • Rates are in basis points (interestRate() is uint16).
  • Per-Trove interest updates only on interaction with that Trove → the storedinterestOwed is stale between interactions. For live entire-debt, read getEntireDebtAndColl, which computes to the current time. Never read the stored value and call it current.
    • CORRECTION (verified on the fork, Phase 2): getTroveInterestOwed and getTroveDebt return the STORED (stale) snapshot, they do NOT compute to now. After a 30-day clock warp, getEntireDebtAndColl.interest grew to ~1.81 MUSD while getTroveInterestOwed stayed 0. So the to-now interest is getEntireDebtAndColl.interest, and live entireDebt = principal + interest from that one getter. musd-kit's getTrove sources interestOwed/principal from getEntireDebtAndColl, never from the stored getters. (Earlier drafts of this section lumped getTroveInterestOwed in with the to-now getter, it is not.)
  • A Trove's fixed rate is set at open from its maximum borrowing capacity at 110% CR, not from the initial draw. refinance moves 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 the Liquidation event): 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 iff ICR < CCR (a Trove with ICR ≥ CCR is never liquidated). Verified normal mode on the fork; RM is inducible via setPrice (TCR < CCR). Useful events: Liquidation(_liquidatedPrincipal, _liquidatedInterest, _liquidatedColl, _collGasCompensation, _gasCompensation) and TroveLiquidated(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); then getApproxHint(partialNICR) → findInsertPosition(partialNICR, approx, approx) → pass (first, upper, lower, partialNICR, maxIterations) to redeemCollateral.
    • FEE, CORRECTION (verified on the fork, Phase 6): the 0%-for-loan-holders rule does NOT hold in this deployment. A redeemer who holds an open loan paid the full borrowerOperations.redemptionRate() = 0.75% (measured from the Redemption event: _collateralFee / (_collateralSent + _collateralFee) ≈ 0.744%). The SDK reads redemptionRate() and applies it to all redeemers (governable, never hardcode 0.75%).
    • truncatedAmount is the redeemable amount given the minNetDebt floor on the last touched Trove (and maxIterations). The Redemption event reports (_attemptedAmount, _actualAmount, _collateralSent, _collateralFee), the actual redeemed amount can be less than truncatedAmount when 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. the minNetDebt floor) redeems exactly truncatedAmount. 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 via claimCollateral(). A liquidation at ICR ≈ 1.0 leaves no surplus.

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 trapThe 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". IMUSDSavingsRate exposes only receiveProtocolYield(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/sdk also 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). Apps pikolo and Mezo-Defi-IQ hand-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 revertForm→ typed error
openTrove with draw+fee < minNetDebtBorrowerOps: Trove's net debt must be greater than minimumrequireBelowMinimumDebt
openTrove/withdrawColl/adjustTrove leaving ICR < MCR (normal mode)BorrowerOps: An operation that would result in ICR < MCR is not permittedrequireICRBelowMCR
openTrove with MCR ≤ ICR < CCR while system in Recovery ModeBorrowerOps: Operation must leave trove with ICR >= CCRrequireRecoveryModeRestriction
repayMUSD more than owedArithmetic operation resulted in underflow or overflow. (Panic 0x11)panicRepayExceedsDebt
repayMUSD with insufficient MUSD heldBorrowerOps: Caller doesnt have enough mUSD to make repaymentrequireInsufficientMusdBalance
op on an address with no Trove (withdrawColl/closeTrove/…)BorrowerOps: Trove does not exist or is closedrequireTroveNotFound
openTrove when one is already openBorrowerOps: Trove is activerequireTroveAlreadyExists
liquidate a healthy TroveTroveManager: nothing to liquidaterequireNothingToLiquidate
redeemCollateral that can redeem nothing (incl. a stale/bad partial hint)TroveManager: Unable to redeem any amountrequireRedemptionFailed

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). StaleHint remains a defined, exported error (public API since Phase 6) but is documented as not distinctly reachable; redemption-hint staleness surfaces as RedemptionFailed.
  • Unauthorized, the SDK surface calls no governance/permission-gated function, so the OwnableUnauthorizedAccount path is unreachable. Defined/exported for completeness only.
  • InsufficientCollateral, the on-chain ICR < MCR revert maps to ICRBelowMCR (contract-authoritative). InsufficientCollateral is retained as the preview-time sibling (for the math/React layer); the write path surfaces ICRBelowMCR.

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.

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