Liquid Network Hack Analysis
The September 6, 2026 at 15:53 UTC, a hacker drained ~4000 Bitcoins (BTC) from the company Liquid Network, using an exploit in the source code of the blockchain, used by this company.

Web3
I’m so fascinated by this world, where every exchange and actions are automated. For me, it is such a beautiful world and technologies that is underlying those concepts of transparency, privacy and decentralization, even if, in practice, it’s just used for (mostly) crappy things like trading and money laundering. But as anything is based on code (blockchain, smart-contracts, …), it is very interesting to study and when this kind of hack happens, I just want to dive deeper in the understanding of the functioning the bug. And man, there are so many money in those web3 chains, that it impressed me every time.
Liquid Network
The blockchain
Liquid Network is hold by a blockchain technology compagny: Blockstream (https://blockstream.com/liquid/). This compagny developed tools and facilities around the Bitcoin ecosystem. Liquid Network is a side block-chain derived form the Bitcoin block-chain, that brings some evolution to the classical Bitcoin, like:
- Confirmation speed around 1 min per block (~10m in for the Bitcoin blockchain)
- Confidentiality for the transactions (public on Bitcoin)
- Stable fees for transactions
- Smart contract, tokens (Bitcoin limited to values) This side chain is indeed more oriented to B2B exchange like trading, use of smart-contracts and with confidentiality.
https://help.blockstream.com/liquid-network/faqs/what-is-the-liquid-network
To operate on the side of the BTC blockchain, bitcoins are transferred to the Liquid Federation wallet, action called “peg-in”, and become LTBC. After operations were made on the Liquid blockchain, the LBTC are released to the BTC blockchain, action called “peg-out”, by being burned by one of the Liquid Federation member, then an equivalent of BTC is send from the Liquid Federation wallet to this member’wallet.
Vocabulary
Some concept inherent to the Liquid blockchain has to be explained to understand what is going under this hack.
-
Confidential Transaction: In the Bitcoin blockchain, everything is public, amounts, etc. In the Liquid blockchain, a confidential transaction hides information like the amount or the asset type from the public.
-
Value Commitment: As the amount is hidden, the value commitment is a mathematical object representative of the amount in the transaction, on which you can make calculation to validate the transaction. It is a commitment because it engaged the secret value, and cannot be changed afterward.
-
Asset Commitment: As the Liquid blockchain does not transport only BTC but also tokens, stablecoins, the asset has to be precised. In the same way that the Value Commitment, the Asset Commitment hides the type of asset of the transaction.
-
RangeProof The value is hidden, but how to be sure that this one is not negative ? The RangeProof is a cryptographic proof that demonstrate that the hidden value is therefore a positive number in a reasonable range. This proof is joined to the transaction.
-
scriptPubKey: It is simply the destination address, to define who will be willing to retrieve the amount.
Technology stack
The Tech stack under the Liquid blockchain is the Open Source project Elements https://elementsproject.org/ This project is OpenSource, you can find the code here: https://github.com/ElementsProject/elements This where I will start to understand how an hacker could steal 4000 of BTC.
Code Analysis
Hypothesis
In the Github repo, many PR seems related to the bug, because of the words “collision” or “harden” in it:
- https://github.com/ElementsProject/elements/pull/1600
- https://github.com/ElementsProject/elements/pull/1601
- https://github.com/ElementsProject/elements/pull/1603
One maintainer said that the PR1600 was accepted as the fix, so let’s see what is the diff. Especially, the function ComputeEntryRangeProof has been modified and heavily commented.

The PR1601 and PR1603 make some statements about the nature of the vulnerbaility.
The bug seems to be that it is possible, in the ComputeEntryRangeProof, to have an identical concatenation of the values proof-commitment-asset_commitment-scriptPuKey that will lead to the calculation on an identical hash in the entry parameter.
After that, I would like to trace back this code to be sure that it is the problem, does the attacker can control those parameters ? With that, I learnt a bit more about the functioning of this blockchain and the operations associated with.
ComputeEntryRangeProof
The function incriminated, SignatureCache::ComputeEntryRangeProof, is located in src/script/sigcache.cpp

As the file title suggests it, this function is linked to the cache and to the calculation of the rangeProof. It is basically a hash of the proof, to create a fingerprint of this one and to be store in the cache for further comparison, by doing a SHA256 of the following value:
- SHA256(salt-proof-commitment-asset_commitment-scriptPubKey)
Why is there a cache ? For performance reason, as in a web browser, you want to keep some results in cache to avoid some network call or heavy calculation to retrieve this result faster.
VerifyRangeProof
This function is called by the parent function CachingRangeProofChecker::VerifyRangeProof. This one is located too in src/script/sigcache.cpp too.

This one is a bit bigger and it starts to show some logic about how is working the Proof cache in the Liquid blockchain.
The first step is to calculate the Proof fingerprint with the previous seen function. The following condition is interesting:
if (rangeProofCache.Get(entry, !store)) {
return true;
}
Here, the code is checking if the RangeProof fingerprint calculated previously is stored in the cache. If yes, the function return true skip all the next cryptographic verifications, that could permits to see if the transaction was valid or not, like the secp256k1_rangeproof_verify.
This architectural choice is make to improve performance as this function implied a lot of calculation (you can see it in the code in src/secp256k1/src/modules/rangeproof/rangeproof_impl.h, function secp256k1_rangeproof_verify_impl).
But we can clearly see here that the first assumption is correct, a similar fingerprint of the RangeProof stored in the cache can totally avoid mathematical verification and be treated valid as a transaction that have been validated and produced the same fingerprint ! This design is vulnerable by default if the attacker controls all the parameters needed: RangeProof, Asset, Asset_Commitment and the scriptPubKey.
CRangeCheck::operator()()
Following the code, the function VerifyRangeProof is used in the operaotr overloading () of the class CRangeCheck in src/confidential_validation.cpp

VerifyAmounts
This operator is used in two functions: VerifyIssuanceAmount and VerifyAmounts. The second one looks exactly where we are looking for, so let’s focus on this one.
It is in the file src/confidential_validation.cpp AND it takes an Confidential Transaction (CTxOut) as an argument !
This (long) function can be divided in 5 parts. In overall, it calculates the transaction inputs and the transaction output, to compare them (like for a sum(input) - sum(output) = 0) and make it valid.
-
Set up value commitments from the Transaction In and the Transaction Out

- Resolve Transactions In (for Assets, Value, …) l173-308
- Resolve Transactions Out l.310-361
-
Balance Check (sum(input) - sum(output) = 0)

-
RangeProof check (vulnerable code!)

- SurjectionProof check
The Transaction classes
Transactions (class CTransaction) used in the VerifyAmounts function, is defined in src/primitives/transaction.h.
This class has for properties a Transaction In (vin) and a Transaction Out (vout).
The class CTxOut represents one output of the transaction, with the address where is supposed to go the amount (One transaction can have multiple address for the output, like sending money to three people with one transaction).

When looking at the constructor of CTxOut, src/primitives/transaction.h, it is pretty obvious that we find three of the values needed in the ProofRange fingerprint calculation.
CTxOut(const CConfidentialAsset& nAssetIn, const CConfidentialValue& nValueIn, CScript scriptPubKeyIn);
If we take back the code in VerifyAmounts, it is effectively this three CTxOut properties that are passed as argument of the CRangeCheck()

For the rangeProof, we can see it comes from ptxoutwit, a CTxOutWitness object.
It is part of the transaction too, there are parameters to ensure that the transactio nis valid. In the original Bitcoin blockchain, only the CtxOut class was existing, the CTxOutWitness was added in the Elements blockchain for the concept of Confidential Transaction.
Conclusion
These 4 parameters are controlled by the user and can lead to a skip of cryptographic verification of the transaction, if a similar fingerprint of a transaction already exists in the cache. This has been implemented of performance reason, but a lack of small check control like the argument length for example lead to a possible fingerprint collisions. By the time I’m writing this small analysis, a more complete analysis of the exploit has been made by the Cervik team: