# PM borrow liabilities are omitted from backing, allowing phantom surplus settlement

MonetrixAccountant credits PM supplied balances but ignores borrow liabilities, inflating surplus so settle() mints phantom USDM yield to sUSDM holders.

- Payout: Contest reward
- Severity: medium
- Program: Monetrix
- Credited: 0xastronatey
- Disclosed: 2026-05-26
- Write-up quality: 7/10
- Original: https://code4rena.com/reports/2026-04-monetrix#m-01
- Library page: https://bugbountylibrary.com/r/c4%3A2026-04-monetrix%3AM-01
- Tags: domain:web3 impact:funds-loss impact:integrity kind:contest platform:code4rena sev:medium tech:solidity technique:source-review vuln:accounting-error vuln:business-logic vuln:oracle-manipulation

**Target:** PrecompileReader 0x811 PM state decoding and MonetrixAccountant backing accounting
**Root cause:** PrecompileReader.suppliedBalance() decodes only the supply field of the 0x811 PM response, discarding borrow basis/value; _readL1Backing() then credits supplied assets as positive backing without ever subtracting PM borrow debt.
**Steps:**
1. Vault has a registered 0x811 PM supplied slot via vault.supplyToBlp() and a nonzero PM borrow liability (Hyperliquid auto-borrows on insufficient balance).
2. Call MonetrixAccountant.totalBackingSigned(): suppliedBalance() decodes `(,,, supplied) = abi.decode(res, (uint64,uint64,uint64,uint64))`, dropping borrow fields.
3. surplus() = totalBackingSigned() - usdm.totalSupply() is inflated; distributableSurplus() inherits it.
4. Call MonetrixVault.settle(proposedYield) after minSettlementInterval (20h); Gate 3 `require(proposedYield <= distributableSurplus())` passes on phantom surplus.
5. distributeYield() mints userShare USDM and injects into sUSDM; insurance/foundation paid in USDC.
6. Attacker (sUSDM holder) cooldowns via cooldownShares(), waits 3-day unstake cooldown, claims USDM, redeems for USDC.
**How found:** Code review of PrecompileReader.suppliedBalance() against Hyperliquid PM docs/API schema: noticed 0x811 response encodes both borrow and supply under tokenToState but the decoder destructures only the fourth uint64 as supplied, then traced that omission into _readL1Backing() and Gate 3.
**Exploitation:** Attacker deposits via permissionless MonetrixVault.deposit() and sUSDM.deposit() to become an sUSDM holder. Vault's PM account drifts into borrow debt. settle() passes Gate 3 because borrow liabilities are omitted from backing, minting phantom USDM yield into sUSDM and paying USDC to insurance/foundation. Attacker cools down sUSDM, claims USDM after 3-day cooldown, redeems excess USDM for USDC.
**Escalation:** Per-window phantom yield bounded by Gate 4 APR cap (~2,876 USDM per 21h at 10M TVL, 12% APR); repeatable each settlement window.
**Insight:** When a precompile/API returns a struct with both asset and liability fields, verify the decoder consumes all fields; a partial abi.decode silently drops debt and inflates any net-equity accounting built on it.
