Summary
A critical vulnerability exists in validate_miner_transaction where the check that enforces the coinbase output amount upper bound is permanently disabled due to an impossible condition. An attacker who can produce a valid PoW block (trivial difficulty during POS fallback) can mint an arbitrary amount of BDX in the miner coinbase output with no cryptographic barrier.
Vulnerability Details
Location
src/cryptonote_core/blockchain.cpp, function Blockchain::validate_miner_transaction
The Bug
uint64_t max_base_reward = reward_parts.base_miner + reward_parts.governance_paid + reward_parts.master_node_total + 1;
uint64_t max_money_in_use = max_base_reward + reward_parts.miner_fee;
if (money_in_use > max_money_in_use && version > hf::hf20_bulletproof_plus)
{
MERROR_VER("coinbase transaction spends too much money...");
return false;
}
The condition version > hf::hf20_bulletproof_plus is permanently false. The hf enum's highest defined value is hf20_bulletproof_plus, followed only by _next and none. No block on any network ever carries a version higher than hf20_bulletproof_plus, so this branch never executes and the enforcement is completely inoperative.
Why the Miner Output Has No Other Protection
The master node hook that runs after validate_miner_transaction explicitly skips verifying the miner's own output amount, with a comment that says it relies on the overall sum-of-outputs check:
// We don't verify the miner reward amount because it is already implied by the overall
// sum of outputs check and because when there are truncation errors on other outputs the
// miner reward ends up with the difference
That "overall sum of outputs check" is the now-dead code above. MN payout outputs and the governance output are correctly verified per-output, but the miner's vout[0] amount has no validation on any currently deployed hardfork version.
Conditions for Exploitation
Under normal post-HF17 POS operation, a PoW block is an alt block that cannot displace a checkpointed POS chain. However the exploit becomes directly viable when:
-
POS liveness fails — The code itself documents this fallback explicitly. When active master nodes drop below 50 (mainnet threshold defined in master_node_rules.h), or when a POS round fails to complete, the network explicitly falls back to accepting PoW blocks. PoW fallback difficulty at this point is POS_FIXED_DIFFICULTY = 1,000,000 — solvable in seconds on commodity hardware.
-
During any period before HF17 — The check was already dead on all prior hardfork versions.
-
On testnet and devnet — Both run the same code. The minimum MN count for POS on testnet is POS_QUORUM_SIZE = 12, easily triggering PoW fallback.
Impact
- Arbitrary BDX minting in the miner coinbase output of a PoW fallback block
- No cryptographic work beyond the
~1,000,000 PoW difficulty required
- MN payout and governance outputs must be correctly formed (they are verified), but the miner's own output is unconstrained
- Inflation of total supply beyond
MONEY_SUPPLY cap
Proof of Concept (Conceptual)
- Wait for or cause a POS liveness failure (active MN count drops below 50, or round timeouts cascade)
- Construct a valid block at the current height:
- Correct
prev_id, timestamp, hardfork version
- Correctly formed MN payout outputs and governance output (amounts verified by MN hook)
- Miner
vout[0].amount set to any desired value (e.g. 10,000,000 BDX)
- Solve RandomX PoW at difficulty ~1,000,000
- Broadcast — the block passes all validation:
- prevalidate_miner_transaction: passes (correct type, version, unlock time, no arithmetic overflow)
- validate_miner_transaction: coinbase cap check is dead code
- MN hook: only verifies MN/governance outputs, explicitly skips miner output amount
Secondary Findings
Finding 2 min_version/max_version variable names swapped (High, latent)
Location: blockchain.cpp:1267-1268
txversion min_version = transaction::get_max_version_for_hf(hf_version); // wrong
txversion max_version = transaction::get_min_version_for_hf(hf_version); // wrong
if (b.miner_tx.version < min_version || b.miner_tx.version > max_version)
Both functions currently return v4_tx_types for all deployed hardforks so the swap is benign today. If a future hardfork introduces a version range where get_min != get_max, the inverted check would reject all valid coinbase transactions and halt the chain.
Finding 3 — coin_burn extra amount not upper-bounded (Medium)
Location: blockchain.cpp:3608, cryptonote_core.cpp:1854
tx_extra_burn.amount in a coin_burn transaction is only checked to be non-zero. There is no check that it is <= txnFee. The actual burn is correctly capped by std::min in fee calculation, but the raw uncapped value is accumulated into burn statistics:
burnt_beldex += static_cast<unsigned long long>(get_burned_amount_from_tx_extra(tx.extra));
Setting tx_extra_burn = UINT64_MAX - 1 causes integer overflow in the get_coinbase_tx_sum RPC response, corrupting reported burn statistics.
Recommended Fix
Fix for Finding 1 (Critical)
Remove the hardfork guard entirely or correct the comparison:
// Before (broken):
if (money_in_use > max_money_in_use && version > hf::hf20_bulletproof_plus)
// After (correct):
if (money_in_use > max_money_in_use)
Fix for Finding 2
Swap the variable assignments:
// Before (swapped):
txversion min_version = transaction::get_max_version_for_hf(hf_version);
txversion max_version = transaction::get_min_version_for_hf(hf_version);
// After (correct):
txversion min_version = transaction::get_min_version_for_hf(hf_version);
txversion max_version = transaction::get_max_version_for_hf(hf_version);
Fix for Finding 3
Add an upper bound check in coinbase tx sum accumulation or in coin_burn validation:
uint64_t burn = cryptonote::get_burned_amount_from_tx_extra(tx.extra);
uint64_t fee = tx.rct_signatures.txnFee;
if (burn > fee) { /* reject */ }
Affected Versions
All versions from HF17 onward. The dead code condition was likely introduced when hf20_bulletproof_plus was added as the final enum value, making > hf20 permanently unreachable.
Note: This report is submitted for responsible disclosure. The vulnerability has not been exploited and no exploit code is included.
Summary
A critical vulnerability exists in validate_miner_transaction where the check that enforces the coinbase output amount upper bound is permanently disabled due to an impossible condition. An attacker who can produce a valid PoW block (trivial difficulty during POS fallback) can mint an arbitrary amount of BDX in the miner coinbase output with no cryptographic barrier.
Vulnerability Details
Location
src/cryptonote_core/blockchain.cpp, function Blockchain::validate_miner_transaction
The Bug
The condition
version > hf::hf20_bulletproof_plusis permanently false. Thehfenum's highest defined value ishf20_bulletproof_plus, followed only by_nextandnone. No block on any network ever carries a version higher thanhf20_bulletproof_plus, so this branch never executes and the enforcement is completely inoperative.Why the Miner Output Has No Other Protection
The master node hook that runs after validate_miner_transaction explicitly skips verifying the miner's own output amount, with a comment that says it relies on the overall sum-of-outputs check:
That "overall sum of outputs check" is the now-dead code above. MN payout outputs and the governance output are correctly verified per-output, but the miner's
vout[0]amount has no validation on any currently deployed hardfork version.Conditions for Exploitation
Under normal post-HF17 POS operation, a PoW block is an alt block that cannot displace a checkpointed POS chain. However the exploit becomes directly viable when:
POS liveness fails — The code itself documents this fallback explicitly. When active master nodes drop below 50 (mainnet threshold defined in master_node_rules.h), or when a POS round fails to complete, the network explicitly falls back to accepting PoW blocks. PoW fallback difficulty at this point is
POS_FIXED_DIFFICULTY = 1,000,000— solvable in seconds on commodity hardware.During any period before HF17 — The check was already dead on all prior hardfork versions.
On testnet and devnet — Both run the same code. The minimum MN count for POS on testnet is
POS_QUORUM_SIZE = 12, easily triggering PoW fallback.Impact
~1,000,000PoW difficulty requiredMONEY_SUPPLYcapProof of Concept (Conceptual)
prev_id, timestamp, hardfork versionvout[0].amountset to any desired value (e.g. 10,000,000 BDX)Secondary Findings
Finding 2
min_version/max_versionvariable names swapped (High, latent)Location:
blockchain.cpp:1267-1268Both functions currently return
v4_tx_typesfor all deployed hardforks so the swap is benign today. If a future hardfork introduces a version range whereget_min != get_max, the inverted check would reject all valid coinbase transactions and halt the chain.Finding 3 —
coin_burnextra amount not upper-bounded (Medium)Location:
blockchain.cpp:3608,cryptonote_core.cpp:1854tx_extra_burn.amountin acoin_burntransaction is only checked to be non-zero. There is no check that it is<= txnFee. The actual burn is correctly capped bystd::minin fee calculation, but the raw uncapped value is accumulated into burn statistics:Setting
tx_extra_burn = UINT64_MAX - 1causes integer overflow in the get_coinbase_tx_sum RPC response, corrupting reported burn statistics.Recommended Fix
Fix for Finding 1 (Critical)
Remove the hardfork guard entirely or correct the comparison:
Fix for Finding 2
Swap the variable assignments:
Fix for Finding 3
Add an upper bound check in coinbase tx sum accumulation or in
coin_burnvalidation:Affected Versions
All versions from HF17 onward. The dead code condition was likely introduced when
hf20_bulletproof_pluswas added as the final enum value, making> hf20permanently unreachable.