Problem
Found on staging while preparing the client test: borrow 0.001 ETH, then "Pay now / repay all" (max: true). Interest makes the debt 0.0010000003 while the wallet holds exactly 0.001, so the repay died with a raw 400 Lending journal would drive USER_CRYPTO(...) negative. A borrower who only holds the principal could never clear a crypto loan with repay-all.
Fix
- Crypto
maxrepay: when the wallet is short andcapacity - round_down(wallet) < debtDust[symbol], repay the wallet balance; settlement's existing DUST write-off clears the remainder. Anything larger is refused (no use of the forgive cap, which could be farmed). - Crypto wallet-short failures now return 409
INSUFFICIENT_WALLET_BALANCE(already handled by the FE), like the fiat rail. - Wallet row read under lock via
LendingJournalService.lockUserWalletBalance(same pair inversion asSupplyService.requestSupply).
Tests
repay.postgres.spec.ts 38/38, borrow.postgres.spec.ts 40/40 (throwaway DB), eslint + build clean, mutation-checked.
Review gate (Opus): PASS after 2 rounds (round 1 removed the forgive-cap branch).