From 91fcd4cbee675d971fc0eac9d507c7f54d60f71b Mon Sep 17 00:00:00 2001 From: JohnnyLawDGB Date: Mon, 6 Apr 2026 17:57:05 -0500 Subject: [PATCH] fix: read system health from stats index in estimatecollateral (Bug #34) estimatecollateral read system health from a static cache (GetSystemMetrics) that is only populated when ScanUTXOSet() runs inside getdigidollarstats or getprotectionstatus. If estimatecollateral ran first, the cache had totalDDSupply=0, causing the health check to hardcode systemHealth=0 (emergency) and apply a 2x DCA multiplier. This doubled the reported collateral requirement even when the system was healthy at 475-528%. The mint transaction builder uses the same path, so valid mints were rejected with "Insufficient funds for collateral and fees." Fix: read totalCollateral and totalDDSupply from the digidollar stats index (fast, already synced) or fall back to ScanUTXOSet(), matching the pattern getdigidollarstats and getprotectionstatus already use. Also change the totalDD==0 case from health=0 to health=30000 (max) since an empty system has nothing at risk. Tested on testnet21: estimatecollateral now returns health=510 / DCA=1x matching getdigidollarstats and getprotectionstatus. Tier-4 mint that previously required 134K DGB now correctly requires 67K and succeeds. Confirmed still present in RC29 (RH-36a fixed a different cache race). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/rpc/digidollar.cpp | 66 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/rpc/digidollar.cpp b/src/rpc/digidollar.cpp index 3a4fa3d3d6..0af96eee5e 100644 --- a/src/rpc/digidollar.cpp +++ b/src/rpc/digidollar.cpp @@ -2567,17 +2567,69 @@ static RPCHelpMan estimatecollateral() int lockDays = GetLockDaysForTier(lockTier); int baseRatio = GetMinCollateralRatio(lockTier); - // Get real system health and DCA multiplier from chain state - // Previously hardcoded to 150/1.0 — caused wrong collateral - // estimates when system health degrades (DCA multiplier increases) - DigiDollar::SystemMetrics metrics = DigiDollar::SystemHealthMonitor::GetSystemMetrics(); - CAmount totalCollateral_est = metrics.totalCollateral; - CAmount totalDD_est = metrics.totalDDSupply; + // Get real system health and DCA multiplier from chain state. + // + // Bug #34 fix: previously this called GetSystemMetrics() without + // populating the cache first. That static cache (s_currentMetrics) + // is only filled when ScanUTXOSet() runs, which only happens inside + // getdigidollarstats and getprotectionstatus. If estimatecollateral + // ran before either of those, the cache had totalDDSupply=0, and the + // check below hardcoded systemHealth to 0 / emergency / DCA 2x — + // even when the system was well-collateralized at 528%. + // + // Fix: read totalCollateral and totalDDSupply from the stats index + // (fast, already synced) or fall back to ScanUTXOSet(), matching + // the same pattern getdigidollarstats and getprotectionstatus use. + CAmount totalCollateral_est = 0; + CAmount totalDD_est = 0; + + { + const node::NodeContext& node = EnsureAnyNodeContext(request.context); + ChainstateManager& chainman = EnsureChainman(node); + + if (g_digidollar_stats_index) { + // Fast path: read from the stats index (already synced) + if (g_digidollar_stats_index->BlockUntilSyncedToCurrentChain()) { + const CBlockIndex* pindex; + { + LOCK(cs_main); + pindex = chainman.ActiveChain().Tip(); + } + if (pindex) { + auto stats = g_digidollar_stats_index->LookUpStats(*pindex); + if (stats) { + totalDD_est = stats->total_dd_supply; + totalCollateral_est = stats->total_collateral; + } + } + } + } else { + // Slow fallback: scan the UTXO set (same as getdigidollarstats) + Chainstate& active_chainstate = chainman.ActiveChainstate(); + active_chainstate.ForceFlushStateToDisk(); + { + LOCK(::cs_main); + CCoinsView* coins_view = &active_chainstate.CoinsDB(); + node::BlockManager* blockman = &active_chainstate.m_blockman; + const CTxMemPool* mempool = node.mempool.get(); + DigiDollar::SystemHealthMonitor::ScanUTXOSet( + coins_view, &active_chainstate.CoinsTip(), blockman, mempool); + } + DigiDollar::SystemMetrics metrics = DigiDollar::SystemHealthMonitor::GetSystemMetrics(); + totalCollateral_est = metrics.totalCollateral; + totalDD_est = metrics.totalDDSupply; + } + } + CAmount oraclePriceMillicents_est = oraclePriceMicroUSD / 10; + // Calculate system health from real chain data. + // When totalDD is 0 (no positions exist), use max health (30000) + // so DCA multiplier is 1x — there's nothing at risk, no reason to + // penalize the first minter with an emergency multiplier. int systemHealth; if (totalDD_est == 0) { - systemHealth = 0; + systemHealth = 30000; } else { systemHealth = DynamicCollateralAdjustment::CalculateSystemHealth( totalCollateral_est, totalDD_est, oraclePriceMillicents_est);