DERO · THE DOUBLE-SPEND, EXPLAINED · 2026-09-01
A bug let the chain apply the same transaction twice, which handed an attacker real spendable coins before the August hard fork closed it. That is not in dispute here and this page does not soften it. What follows is the mechanism, the amounts, the arithmetic that ties them together, and an honest account of which parts nobody can see.
A proof-substitution bug made it possible to get one transaction applied to balances twice. The
transaction the community flagged, 2e2b49f2…b21269ac, appears under the same ID in
two different canonical blocks about 26 seconds apart:
| Block | Block hash | Contains txid | On main chain |
|---|---|---|---|
| 7,471,053 | 443d44c6… | 2e2b49f2…b21269ac | yes |
| 7,471,055 | a78e7263… | 2e2b49f2…b21269ac | yes |
Neither block is an orphan or a side block, and both sit well before HF3 activated at 7,504,640. The August hard fork closed the tampered-proof variant, and a plain replay is refused because the sender’s account nonce has already advanced, so no one can do this on the chain as it runs today.
A normal transfer moves 100 from a sender to a receiver. Applied twice, the receiver is credited 200 and the sender is debited 200. But the sender only had 100, so it drops to −100. A negative balance cannot be spent, so that account is dead. Watch the three columns: the two balances move wildly, the total never does.
The same thing with the real anchor amount. A single application moves 1,194,726.1 DERO once; the exploit applied it twice.
| Balance | Before | Normal tx | Applied twice |
|---|---|---|---|
| Sender | 1,194,726.1 | 0 | −1,194,726.1 |
| Receiver | 0 | 1,194,726.1 | 2,389,452.2 |
| Total | 1,194,726.1 | 1,194,726.1 | 1,194,726.1 |
A cheque that clears, drawn on an account that is closed and overdrawn. The cash spends perfectly well; the account behind it is dead and will never be collected. Every created coin is that cheque, paired with a frozen negative of exactly its size. And because dead debt has no ceiling, this can pile up past the total number of coins that were ever issued.
Spendable money with a dead twin: real to spend, invisible to the total, unlimited because dead debt has no cap.
These coins passed the chain’s own proofs. They sit in valid accounts and they can genuinely be moved and sold. Nothing here is a rendering bug or a mislabelled field, and treating it as one is the fastest way to lose an argument with someone who has read the chain.
How the second copy reached a block is a separate question from what it did to the ledger, and for an operator it is the more alarming half. A DERO node will not re-admit a transaction it has already seen mined, and a miner will not select one that appeared in the previous twenty blocks. So these second copies did not come from stock software. Each was placed by a modified block producer, carrying a substituted proof that flips which half of the ring the replay check inspects. Somebody was running that producer on mainnet for six days. The source-level detail, with the exact guards and line numbers, is not published on this page.
A byte-identical, already-mined transaction forced back into a new block is refused. But the thing that refuses it is a pre-HF3 replay check. The natural assumption that HF3 closed re-inclusion is wrong. HF3 closed the tampered variant. The plain replay is caught because the sender’s account nonce has already advanced, so the replay’s reference nonce is stale and the block is rejected.
Measured on the simulator under the shipped post-HF3 rules. The transfer is 5 atomic units, the
receiver starts at 100,000,000, and every row is the real return of Add_Complete_Block
with balances read back off the tree.
| Control | Presented | Proof path | Nonce check | Receiver balance | Applied |
|---|---|---|---|---|---|
| C1: honest, once | genuine | ran, cached | ok | 100000000 → 100000005 | 1× (+5) |
| C2: tampered, same ID | proof field altered (f[0]=1) | not reached | not reached | unchanged | 0, rejected |
| C3: identical replay | byte-identical | cache hit, skipped | Invalid Nonce (exp 2, got 0) | stays 100000005 | 1×, no double |
| C4: replay, nonce neutered | byte-identical | cache hit, skipped | disabled (mutation) | 100000005 → 100000010 | 2× (+10) |
C4 is the control that makes the rest mean something: disable that one nonce comparison, change nothing else, and the same replay is credited twice from a single transfer. That proves the comparison is what holds the line. The whole table was re-run at ring size 16 and reproduces exactly, so it is not a ring-2 artefact. The worry that HF3 was insufficient did not hold up.
Do that doubling 46 times across four chained ladders, each one a run of fresh accounts where the doubled output of one hop funds the next, and the created spendable adds up to about 23.9M DERO. So do the frozen negatives, coin for coin. Add every account on the chain together and the two sides cancel, so the ledger’s own arithmetic is undisturbed while 23.9M of real spendable coins sit above it.
That is also why the created total can exceed every coin DERO has ever issued. For real coins that would be impossible. It is possible here precisely because they are not backed by real coins.
Were spendable coins created?
Yes
23,886,517.3, at least
Double-credited into valid balances the holder can move or sell.
Does the ledger still balance?
Yes
matched pair by pair
Each created coin is matched by an equal frozen debt, so the two sides cancel.
Same event, two questions. One asks what exists and can be spent; the other asks whether the books tie out. Yes to both, at the same time. Only one of those answers is reassuring, and it is the one about the books.
Of the issuance schedule: no. It was never altered, and that figure was never going to move whatever happened. Of the ledger’s balances: no. They still tie out, because the frozen debt cancels the created coin. Of spendable coins: yes. About 23.9 million now exist that should not, and that is the answer that matters to anyone holding or pricing DERO. “They minted coins” and “no inflation, the supply is intact” are each one third of this, which is why the argument never resolves. The honest statement is all three at once.
This gets misused in both directions, so it is worth being exact. When a node answers
DERO.GetInfo with total_supply, this is the whole of what it does:
result.Total_Supply = blockchain.CalcSupply(uint64(chain.Get_Height()))
result.Total_Supply = result.Total_Supply / 100000 // atomic units to DERO
func CalcSupply(height uint64) uint64 {
supply := config.PREMINE
supply += 271739600 // one-time launch credit
for ... { supply += CalcBlockReward(epoch) * blocksInEpoch }
return supply
}
The only input is the block height. It reads no balances, no transactions and no burns. It would have printed the same figure whether this exploit happened or not, and it does not move when coins are destroyed either. Recomputing it offline and finding a match shows two calculators agreeing, nothing more. There is no balance census in the software, and on a chain where every account balance is encrypted there is no obvious way to build one, so the supply can be neither proven correct nor proven wrong by counting. What is left is transaction-level evidence, which is what the companion document sets out in full.
A transaction mined at block 7,539,913 carries a burn of exactly 500,000,000,000 atomic units. You can read that number straight from the transaction, but that is not why it can be trusted: the chain would have refused the transaction unless the coins had actually left the sender's balance, because it checks the burned amount against a real deduction rather than taking the field on faith. That is a fifth of everything the exploit made, gone permanently. The Tracked Flow, the companion record, breaks the remainder down and draws the proportions to scale.
At block 7,545,119 the schedule has issued 16,756,595. Take out the 12,281,254 premine and the one-time launch credit shown in the code above, and about 4,472,624 DERO has ever been mined on this chain. A burn of five million is larger than all of it put together, and close to a third of every DERO in existence. Because the chain accepted the transaction, whoever sent it was holding at least that much in one wallet at that moment.
That is the reason to think the burn came from this operation. It is not proof of it. Nobody can show the destroyed coins came from these wallets, because the burn conceals its sender among sixteen candidates. Its size makes an innocent explanation hard to construct, and difficulty is not evidence.
The created total is a book figure, not a cash-out. A market that trades around seventeen million coins cannot absorb twenty-four million without the price collapsing first, so what was actually realised is far smaller. How much smaller is unknowable, because these amounts are encrypted, which means nobody can put a verified number on it, including the people claiming they can.
A question that keeps coming up: can the rest be returned? There is no one to return it to. These coins did not come out of anybody’s wallet. They were conjured, and matched by an equal negative in the account the same act froze, so there is no victim to reimburse and no previous owner to restore. That leaves exactly one way to take them out of circulation, which is to destroy them, and five million already has been. Whatever is still outstanding stays outstanding until someone chooses to burn it too.
Only a fraction can be read, and only because these are pre-2025 transactions with a ring size of 2, whose payload key is recoverable by brute force. DERO closed that hole over a year ago, so a transaction from a current wallet keeps its amount private. That is what pins the attacker’s tooling to code from many releases back. The honest caveat: the double-application itself was a consensus bug affecting every node regardless of transaction format. Old wallet code is why these particular amounts are readable. It does not mean only old-wallet users were exposed, and it does not make the fix optional.
Worth saying what does not establish these figures: the deroproof strings that
circulate beside them. Such a string carries both its own declared amount and the value that amount
gets checked against, so one can be built to display any number at all and still show as verified.
What actually fixes an amount is the transaction itself, whose payload unscrambles into a clean,
canonical message under one value and into noise under every other.
Issued per schedule
16,756,595
What the emission schedule says has been minted by block 7,545,119. Exact and recomputable, but arithmetic on the height rather than a count of anything.
Created, less the burn
18,886,517.3
Made from nothing by the double-application, less the five million burned. That subtraction assumes the burn came from these wallets, and nothing on chain shows that it did. A floor either way: fourteen affected transactions cannot be read at all.
Spendable, roughly
~35.6M
The schedule figure plus the created coin less the burn. The first term is not a count of anything, as the section above sets out, so treat this as a floor on what can circulate rather than a measurement. It says the created coin sits on top of normal issuance. It does not say anyone has counted 35.6 million coins. Approximate in both directions: undecoded transactions push it up, coins stranded in frozen accounts push it down.
2e2b49f2…b21269ac in their transaction set.
Same ID, two blocks.808342e3…3e7898 and look for
8090cad2c60e in the raw hex. That is a varint and it decodes to 500,000,000,000 atomic
units, exactly 5,000,000 DERO.DERO.GetInfo reports
total_supply 16,756,595 at block 7,545,119, rising 0.3075 per block. Read the section
above before treating that number as evidence of anything.Everything above is a matter of fact. What follows from it is not. The created coins are real, spendable, and permanent unless someone burns them or returns them to the accounts they were drawn from, and nobody can identify those accounts. So the question in front of DERO is not whether this happened. It is whether coins that exist and can be sold should be counted in the figure the project publishes as its supply.
There are three answers and none of them is free. Leave the schedule figure alone and explain every time what it does and does not count. Change what the daemon reports, which breaks the one thing that figure is currently good for, namely stating the issuance schedule exactly. Or publish a second figure beside it and keep it current as coins are burned or found. That is a governance call rather than a forensic one, and this document does not make it.
Parts of this are permanently out of reach, and saying so is better than implying a complete picture. Every figure on this page is a floor.
The Tracked Flow sets each of these gaps against the specific transactions where they occur.