At 14:28:56 UTC on 6 September 2026, Bitcoin block 965,783 released 3,996.02 BTC from the Liquid Federation's wallet to an address the federation did not control. Eleven of its fifteen signers had approved the withdrawal, and every signature was valid. Nobody had stolen a key or broken into a server.
What the signers had approved was a legitimate-looking request to redeem tokens that should never have existed. This post explains how those tokens were created, why the patch meant to prevent exactly this caused it instead, and what the incident says about multisig custody.
How Liquid is meant to work
Liquid is a sidechain built by Blockstream. You lock bitcoin with a group of fifteen operators called the federation, and an equal amount of a token called L-BTC is issued to you on the sidechain. Going the other way, a peg-out, destroys your L-BTC and releases the matching bitcoin. The whole arrangement depends on one rule: there must never be more L-BTC in circulation than there is bitcoin in the federation's wallet.
Liquid also hides transaction amounts. Since you cannot see an amount, every output ships with a mathematical proof called a range proof, showing that the hidden number is a real and sensible amount. Checking those proofs is slow, so the software remembers results it has already checked and skips the work when the same proof turns up again.
The flaw: a shortcut that skipped the check
To remember a result, the software needs a label for it. That label was built by mashing four pieces of data together into a single hash, with nothing to mark where each piece ended:
hasher.Write(proof)
.Write(value_commitment)
.Write(asset_commitment)
.Write(scriptPubKey)
.Finalize(); // no length delimiters
Two of those pieces vary in length. As a result AA BB | CC DD and AA BB CC | DD
produce exactly the same label even though they describe different things. Shifting a few bytes across the
boundary makes two unrelated items file under one name.
The stored result is a pass, so a label collision does not give a wrong answer. It removes the check entirely. The software reports "verified" for a proof it has never looked at. CertiK described it as "an ambiguous cache-key encoding" that "fed the raw fields into one SHA-256 stream without encoding their lengths."
The attack follows from that. Send a normal transaction so a genuine pass gets filed, then send a second transaction crafted to land under the same label. The software looks up the label, sees a pass, and accepts a proof for whatever amount the attacker chose.
How the attack ran
Timing was tight, because a result is only remembered while the first transaction is still waiting to be confirmed, a window of roughly sixty seconds. The attacker spent about fourteen hours beforehand broadcasting 68 cheap transactions carrying identical proofs, apparently tuning the collision. Then, on 6 September (UTC):
- 13:52:10. The priming transaction, which files a genuine pass.
- 13:53:10. The colliding transaction mints about 3,998.5 L-BTC out of nothing.
- 14:01 and 14:06. A 2.65 BTC test peg-out, then 3,996.02 BTC.
- 14:28:56. The bitcoin lands on the main chain in block 965,783.
The whole sequence took thirty-seven minutes.
The fix caused the bug
Most coverage described this as old code that nobody had noticed. That is not what happened.
- March 2019. A commit simplifies the label, dropping two of the four fields. Call it Bug A. It sits there for seven years.
- 3 August 2026. Commit
c26d719c29, "fix: range proof cache bind to asset and scriptpubkey," puts the two fields back, but mashed together without length markers. That is Bug B, and Bug B is the one that was used. - 1 September. Merged to the public repository under that descriptive title.
- 6 September. Exploited.
"I've seen claims that this was a long-standing bug... That does not appear to be true. Instead, it seems that the fix was deployed, but inadvertently introduced a new bug which was subsequently exploited."
mononaut, mempool.space
Independent analysis of the commits agrees on the mechanism. The pre-fix label could not collide in this way, so the attack requires the patched code. One side effect was that nodes still running official releases did not have the bug. They correctly rejected the exploit block and stalled, which split the chain until block production was halted at 04:49 UTC on 7 September.
On-chain fund flow
The transaction graph is short and shows no attempt at obfuscation. There were two peg-outs of 2.65 BTC and 3,996.02 BTC, one consolidation, and then a single transaction the following morning that contained both the return and the retained change output.
Fund flow: federation peg wallet to attacker and back
Times UTC · arrow thickness proportional to BTC moved
View as table
| Time (UTC) | Flow | Block | Amount |
|---|---|---|---|
| 6 Sep 14:01 | Federation peg wallet → Peg-out address · Test peg-out | — | 2.65138358 |
| 6 Sep 14:28:56 | Federation peg wallet → Peg-out address · Main peg-out settles | 965,783 | 3,996.01834922 |
| 6 Sep | Peg-out address → Consolidation address · Consolidated | 965,783+ | 3,998.67 |
| 7 Sep 11:46 | Consolidation address → Liquid Federation · Returned to the federation | 965,950 | 3,400 |
| 7 Sep 11:46 | Consolidation address → Retained by attacker · Retained as change | 965,950 | 598.49955894 |
| — | Blockstream · “Please contact [email protected]” | 965,822 | 0.00001 |
| — | Attacker · “Fix the bug first… make sure every node is patched” | 965,875 | 0.00001 |
| — | Blockstream · “Bridge nodes are patched, safe to return the funds” — PGP-signed | ~965,908 | 0.00001 |
bc1qgslsydz…yc6wt7p
with a later consolidation, while CertiK and Bitquery cite bc1ql4mfu6…gyqjlte, the address
carrying the "we are whitehats" OP_RETURN. Both are attacker-controlled, so both are shown.
Scroll the diagram horizontally on narrow screens.
A negotiation written into the blockchain
The address holding the funds carried a message: "we are whitehats. contact us on chain." Over about twelve hours the two sides exchanged 21 messages, each paid for at 1,000 satoshis and written permanently into Bitcoin, starting in plaintext and later switching to PGP. Blockstream opened with "Please contact [email protected]". The reply:
"Please fix the bug first. The chain is under risk at latest commit right now. Make sure every node is patched. Then we will transfer the money back safely after confirming the fix."
attacker, Bitcoin block 965,875
Once Blockstream confirmed the patch in a message clear-signed with its published PGP key, 3,400 BTC came back at 11:46 UTC on 7 September. The same transaction kept 598.49955894 BTC as change.
Bounty, or not
598.49955894 is exactly 15% of the take. Nobody involved has published a bug-bounty programme covering it. In the visible record Blockstream agrees only to patch, while the attacker sets the figure and asks "Sending most back...is that ok" afterwards. Ledger CTO Charles Guillemet publicly rejected the white-hat label and argued the arrangement was closer to extortion. Four encrypted messages followed the return, so the public record is not the whole conversation, but on the available evidence the 15% was self-awarded rather than agreed.
Where things stand
About 4,200 L-BTC remain in circulation against just under 3,600 BTC, which works out to roughly 86 cents of bitcoin behind every L-BTC. As of 9 September peg-outs are paused and nobody has announced a plan to cover the gap. Reporting has flagged the risk that L-BTC trades at a discount once withdrawals reopen.
The patch shipped on 8 and 9 September as Elements Core 23.3.4. It length-prefixes every field in the label so distinct items can no longer collide, and it adds a switch to turn the cache off without recompiling. There is still no CVE and no security advisory. The release notes carry no security label, and Blockstream's own blog said nothing about the incident; statements went out on X instead.
Why fifteen signers did not help
Requiring eleven of fifteen signatures protects you when someone steals keys, on the assumption that the signers can fail independently. Here they could not. Every honest signer was running the same software and consulting the same faulty shortcut, so all of them saw the same fake tokens as real. Nobody disagreed, so the threshold was never really tested.
The usual defences would have made no difference here. Hardware security modules, signers in different countries and key rotation all guard against stolen keys, and no key was stolen. The second lesson concerns release process. A security fix reached production ahead of any tagged release, introduced a worse bug than the one it closed, and sat in a public repository under a descriptive title for five days before anyone used it.