Skip to content

Critical: Coinbase overspend check is dead code unlimited BDX minting possible via PoW fallback block #188

Description

@HYDRA-AA

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:

  1. 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.

  2. During any period before HF17 — The check was already dead on all prior hardfork versions.

  3. 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)

  1. Wait for or cause a POS liveness failure (active MN count drops below 50, or round timeouts cascade)
  2. 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)
  3. Solve RandomX PoW at difficulty ~1,000,000
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions