An error a few bytes long: a review of the attack on Liquid Network

An error a few bytes long: a review of the attack on Liquid Network

A vulnerability in the Liquid Network code made it possible to create about 3,998.5 L-BTC by exploiting a flaw in the caching mechanism for cryptographic checks.

Sep 9, 2026

On September 6, 2026, the Liquid Network was attacked through a vulnerability in the Elements codebase, which is used to verify confidential transactions.

The problem arose from the ambiguous construction of the cache key when verifying a rangeproof — the cryptographic proof that confirms that a hidden transaction amount is permissible. Because of this, two different sets of verification data could produce the same cache entry.

First the attacker prepared special transactions and filled the cache with them, and then sent a malicious transaction that increased the volume of assets issued: it reused an already stored verification result and made it possible to create unauthorised L-BTC.

After that, the attacker exchanged these assets inside the Liquid network for real bitcoins through the mechanism for withdrawing funds to the Bitcoin mainnet.

Background

Bitcoins locked in a dedicated Bitcoin wallet controlled by the Liquid federation back the L-BTC issued on Liquid. Under the peg system in place, one L-BTC must correspond to one BTC held under the federation's control.

Instead of publishing the amount, a transaction can contain a Pedersen cryptographic commitment. The asset itself can be represented by a similar commitment. A special mathematical equation confirms that the incoming and outgoing values of the transaction are balanced, while the so-called rangeproof demonstrates that every hidden amount lies within the permitted non-negative range.

In particular, rangeproof verification is part of the mechanism that is meant to guarantee that a hidden amount really is correct and cannot be used to create assets beyond the permitted quantity. At the same time, the actual correctness check depends on several elements at once: the rangeproof, the value commitment, the asset commitment and the scriptPubKey.

The script is passed to secp256k1_rangeproof_verify as additional data included in the cryptographic check. A proof that is valid for one combination (C, X, S) does not automatically become valid for another combination.

Key transactions

Date/timeTXIDDescription
2026-09-06 13:52:10271147100a94f6337b6c3db39b30c92d5b97ed91597307b6f721f73a15187ec5Preparatory transaction
2026-09-06 13:53:10f24a4b179b5cc7e88b25a763911f7cbdf2bf45d1d1b5ab611e94461cef0a183fThe transaction that created additional L-BTC inside Liquid
2026-09-06 13:54:103875a6d6ed4af708e6fd90d1c5504dc014e52c7093a566987252006d6cf1146bThe transaction creates new confidential Liquid outputs controlled by the attacker or by the next wallet
2026-09-06 14:00:10c6ea588ac26f5838b6acbb2a444a33b325bbfeb39bf16dfe27b47215ffd72267The transaction creates new confidential Liquid outputs controlled by the attacker or by the next wallet
2026-09-06 14:01:1046f117c990580501a5156937a8c8affda551a38b3c0b99d29eb8469ee6beb3d2Withdrawal from Liquid of 2.65138358 L-BTC
2026-09-06 14:06:10ce4caece413cd9d444ce7ed9f54e5b328b3da5e4af301aff59a3571f76e988f2Withdrawal from Liquid of 3,996.01834922 L-BTC
2026-09-06 14:28:5685d2ca15bea33a592e73ed40c6a5da887feecf1e77f58ec7f580e008416450433,996.01834922 BTC sent to the address bc1ql4mfu6aundtkksxklfajs2h3t9nzcd6gyqjlte
2026-09-07 18:09:25a6d697a25266ce3c78774fd1d75f896b7af522ada209b0f6228ea497bc49a46d3,400 BTC returned to the address bc1qdlld6antmv4xug242ed83q7k4rqw50cwfns38szx4qu2f4jwaxxsuhwxxr

Key addresses

AdressPurpose
ex1q7kgx4ptje7px48tn0nsmc6se5pngdp3smpqa2wThe attacker's address on the Liquid network
bc1qgslsydz56d0ed6827hdemfmk5w2f6ldyc6wt7pRecipient of the federation payout of 3,996.01834922 BTC
bc1ql4mfu6aundtkksxklfajs2h3t9nzcd6gyqjlteRecipient of the federation payout of 2.65138358 BTC
bc1ql4mfu6aundtkksxklfajs2h3t9nzcd6gyqjlteThe attacker's address for consolidating funds and passing messages on the blockchain
bc1qdlld6antmv4xug242ed83q7k4rqw50cwfns38szx4qu2f4jwaxxsuhwxxrThe Liquid federation's Bitcoin wallet used for the peg and for withdrawals

1. Forging the proof cache

For the analysis, the raw Elements serialisation data was obtained and parsed, not only the fields displayed by a blockchain explorer. The :0 outputs of the two preparatory transactions match completely in asset, value commitment, nonce, script, surjection proof and rangeproof.

Using zero-based offsets and an exclusive upper bound, output_0 of transaction 271147100a94f6337b6c3db39b30c92d5b97ed91597307b6f721f73a15187ec5 was decoded from the 8,882 bytes of the raw transaction, which produced the following data:

a. C0 = 09d6c6150e95e576d288dcc890bf48518119c6f227ea1be6f9a932792dbee683f5

b. S0 = 6a|43|086f5d67160fc4b477954fb09ef321e5b589d7a07740a1a6df494ed2335b1d01d8|0a0a488de4899d0ae 757f6cf8368663184d164106111ed9eaecf510e35282ddc6d|6a.

i. 6a stands for OP_RETURN.

ii. 43 means that the next 67 bytes must be pushed.

These 67 bytes deliberately have the following structure: a 33-byte C1, a 33-byte X and a single-byte 6a. The script is therefore S0 = 6a43 || C1 || X || 6a, where X = 0a0a488de4899d0ae757f6cf8368663184d164106111ed9eaecf510e35282ddc6d is the result of secp256k1_generator_generate(L-BTC asset ID).

2. Filling the cache in the live mempool

When a block is connected, the cache rules are such that an existing entry is consumed, but if there is no entry, a new one is not stored. The connection of block 4,050,335 should therefore have removed the cache entry created when the two visible preparatory transactions were first accepted.

Consequently, for the transaction creating additional L-BTC to pass verification after block 4,050,335 was connected and before the invalid output was checked, every node accepting the transaction needed a new valid transaction with the same sequence (P0, C0, X, S0).

The identifier of this filler transaction and its original bytes are not yet known. Finding them may require the debug.log file of one of the functionaries, the mempool.dat that was current at that moment, a record of the data exchange between nodes, or a saved copy of the mempool state.

3. Creating additional L-BTC inside Liquid

Decoding output_1 of the inflation transaction f24a4b179b5cc7e88b25a763911f7cbdf2bf45d1d1b5ab611e94461cef0a183f produced the following data:

a. C1 = 086f5d67160fc4b477954fb09ef321e5b589d7a07740a1a6df494ed2335b1d01d8.

These are exactly the first 33 bytes of the data passed by the OP_RETURN script of the preparatory output.

It should be noted that the rangeproof P1 is 68 bytes longer than P0, and this additional data sits at the end: 09d6c6150e95e576d288dcc890bf48518119c6f227ea1be6f9a932792dbee683f5

b. 0a0a488de4899d0ae757f6cf8368663184d164106111ed9eaecf510e35282ddc6d6a43, which yields C0 from the preparatory transaction:

09d6c6150e95e576d288dcc890bf48518119c6f227ea1be6f9a932792dbee683f5

and X = 0a0a488de4899d0ae757f6cf8368663184d164106111ed9eaecf510e35282ddc6d is the result of secp256k1_generator_generate(L-BTC asset ID). Embedding the exact L-BTC generator allowed the script bytes of the preparatory transaction to line up with the asset generator field in the attacking transaction.

c. Overall

A valid preparatory transaction: P0 || C0 || X || (6a43 || C1 || X || 6a)

An invalid attacking transaction: (P0 || C0 || X || 6a43) || C1 || X || 6a

Both ultimately reduce to the same 4,301-byte stream: P0 || C0 || X || 6a43 || C1 || X || 6a

4. Withdrawing the funds

L-BTC can be exchanged back for real BTC held under the control of the Liquid federation. The next transaction, 3875a6d6…f1146b, spent output f24…:0 of the transaction linked to the attack. Two more transactions created explicit outputs for withdrawing funds to the Bitcoin mainnet:

  • 46f117c9…eb3d2: 2.65138358 L-BTC
  • ce4caece…988f2: 3,996.01834922 L-BTC

The Bitcoin transaction 8db751a6…7b140, confirmed in block 965,783, paid out the two exact amounts withdrawn as a result of the attack.

What was at the core of the vulnerability?

Elements caches successfully verified rangeproofs so as not to perform the same expensive cryptographic operation twice. Two designs matter here.

That design did not account for the asset generator and the script, even though both parameters affect the validity of the proof. Commit c26d719 attempted to bind the cache entry to the entire verification context.

The patch with the cache implementation

A valid preparatory transaction: (P0, C0, X, 6a43 || C1 || X || 6a)

An invalid attacking transaction: (P0 || C0 || X || 6a43, C1, X, 6a)

The observed outputs of the preparatory and the attacking transactions do not create a collision when the older P || C key is used. Their inputs for the old key differ both in length and in content.

The construction of the attack, by contrast, matches exactly the four-field key without separators introduced by this fix. A transaction passing successfully therefore points to an implementation similar to c26d719 combined with a pre-filled cache. The exact versions of the binaries deployed on individual Liquid functionary nodes have not been publicly disclosed.

Synchronization problem: how the Verus protocol lost $7.5M due to differences between blockchains

Movement of the funds

Address: bc1ql4mfu6aundtkksxklfajs2h3t9nzcd6gyqjlte.

#Hack
Liquid Network was hacked because of a cache: what happened to 4,000 BTC?