On September 24, 2026, attackers drained about $387.5 million from Bitget. Five days later, the snapshot for its 47th proof of reserves report put the overall reserve ratio at 131%. Withdrawals were still coming back one asset at a time.
I found that pair of numbers more instructive than any explainer. The exchange had lost a large sum and was still holding more than it owed, at least at 09:00 UTC on one Tuesday. What the ratio couldn't say was how the attackers got in, or what the balance sheet would look like the following week.
My last two posts dealt with a single transfer, before and after you press Send. This one is about the platform that holds the balance in the first place. If you build on exchange APIs, or simply keep coins on an exchange, you should know what a proof of reserves report covers and where it stops.
Key takeaways
- Proof of reserves has two halves: proof of assets (the exchange controls these wallets) and proof of liabilities (this is what it owes customers). A ratio needs both.
- A Merkle sum tree lets each user check that their own balance was counted, without the exchange publishing everyone's balances.
- The classic weak spot is a fake negative balance hidden in the tree. Zero-knowledge proofs close it, which is why OKX moved to zk-STARK proofs.
- A report is a snapshot. It doesn't cover borrowed assets moved in for the day, fiat, off-chain liabilities or operational security.
- Bitget's 131% after a $387.5 million theft is a good case for seeing what a ratio shows and what it leaves out.
Two halves of one number
A reserve ratio is assets divided by liabilities, calculated per asset, and each side has to be proven separately.
Proof of assets shows that the exchange controls specific on-chain addresses. Usually it publishes the addresses and signs a message with each key. OKX, for example, provides open-source tools to verify signed "I am an OKX address" messages and to compare balances against a block-height snapshot (OKX).
Proof of liabilities shows what the exchange owes its users, and it's the harder half. Publishing every customer balance would wreck privacy, so the usual approach is a Merkle sum tree, which Vitalik Buterin walked through in his 2022 post on proof of solvency.
OKX's 47th report lists 109% for BTC, 101% for ETH, 105% for USDT and 100% for USDC. You can only read those figures as reserve ratios because assets and liabilities were proven for the same snapshot.
How a Merkle sum tree proof works
Each user becomes a leaf: a hash of a salted user identifier plus that user's balance. Every parent node stores a hash of its children and the sum of their balances, so the root ends up committing to total liabilities.
The exchange publishes only the root. You get a proof made of the sibling nodes along the path from your leaf to the root. A verification tool recomputes that path, and if it lands on the published root, your balance was counted.
Here is the check in TypeScript. Real exchanges use their own leaf formats, so read this as the shape of the verification rather than a drop-in tool:
import { createHash } from 'node:crypto'
type Node = { hash: string; sum: bigint }
type Step = { sibling: Node; siblingOnLeft: boolean }
const sha256 = (s: string) => createHash('sha256').update(s).digest('hex')
export const leaf = (userIdHash: string, balance: bigint): Node => ({
hash: sha256(`leaf|${userIdHash}|${balance}`),
sum: balance,
})
export function parent(left: Node, right: Node): Node {
if (left.sum < 0n || right.sum < 0n) throw new Error('Negative sum in tree')
return {
hash: sha256(`node|${left.hash}|${left.sum}|${right.hash}|${right.sum}`),
sum: left.sum + right.sum,
}
}
export function verifyInclusion(start: Node, path: Step[], root: Node): boolean {
let node = start
for (const { sibling, siblingOnLeft } of path) {
node = siblingOnLeft ? parent(sibling, node) : parent(node, sibling)
}
return node.hash === root.hash && node.sum === root.sum
}
Balances are bigint in the smallest unit of the asset, since floating point has no business near money. The sum also goes into every hash, so changing a subtotal anywhere breaks every path that runs through it.
The negative balance trick
Suppose an exchange holds 890 ETH but owes customers 1,390 ETH. It can add a fake account with a balance of -500 ETH, and the root will show liabilities of 890 ETH, matched exactly by reserves.
In a plain Merkle sum tree, the defense is the check you see in parent: a negative sum throws. A user whose path passes near the fake account sees the negative value and rejects the proof. But that only helps if someone in that branch actually runs the check, and an exchange can bet that nobody will. Buterin points out that quietly leaving those users out of the tree would work just as well for a dishonest exchange.
Zero-knowledge proofs take that bet off the table. A zk proof can show that every balance in the tree is non-negative and that they add up to the claimed total, without revealing any individual balance. OKX moved its liabilities proof to zk-STARKs. For reports since September 2024, users download an inclusion proof file and run it through an open-source validator, and a separate file proves the total and the non-negative constraint (OKX).
What a reserve ratio can't tell you
Proof of reserves became the standard reassurance after FTX collapsed in November 2022. A month later the accounting firm Mazars paused this work for all its crypto clients, citing concerns about "the way these reports are understood by the public." Its reports followed agreed-upon procedures, were not audits, and described a single past point in time (CNBC).
Four years on, the gaps are the same:
- Any day other than the snapshot. Assets can be borrowed for the snapshot and returned afterwards. Buterin's suggested fix is frequent or coordinated proofs, so two exchanges can't count the same funds.
- Fiat. Bank balances can't be proven cryptographically. At best they rest on bank confirmations and an auditor's attestation.
- Liabilities outside the tree. Loans, obligations to affiliates or market makers, and anything else that isn't a customer balance won't show up in a customer liabilities tree.
- Key security. A wallet can be fully reserved and badly protected. Bybit lost about $1.5 billion in ETH in February 2025, in an attack the FBI attributed to North Korea.
- Your own inclusion. If you never check that your balance is in the tree, the proof did nothing for you.
Bitget as a worked example
After the September 24 theft, Bitget covered the losses from its protection fund, which had held 5,500 BTC beforehand. On September 30 it said the fund was back at $309 million. Withdrawals resumed for BTC on September 28, ETH on September 29 and USDT on September 30, with other tokens, fiat and P2P following on October 2 (The Crypto Times).
The 47th report, snapshot at 09:00 UTC on September 29, gave an overall ratio of 131% across 19 assets, down from 135% in the previous report. BTC stood at 142%, ETH at 110%, USDT at 107% and USDC at 154%. In the 24 hours before the snapshot, net outflows came to about $463 million.
So Bitget stayed solvent through a theft of that size and a run of withdrawals, and the report shows it. For how the attackers got in, how fast the fund gets rebuilt, or what the ratio looked like on September 23, you need other sources.
Reserves are one column in the table
A recent comparison of top crypto exchanges for users in Asia scores six platforms on five public checks: trading volume share, regulatory access in Asian markets, proof of reserves and protection funds, base spot fees, and incident record since 2025. I like that reserves and incidents sit side by side there. Bitget's 131% reads differently once you know the exchange had just paid out $387.5 million from its own fund.
If you build for Asia, the regulatory column deserves the same attention, because access changes from one country to the next. The comparison tracks platforms that are licensed in one market, blocked in another and winding down in a third.
I also noticed how the table handles a non-custodial swap service: its reserves column is marked not applicable. That's the right call. A service that doesn't hold user balances has no liabilities tree to publish, and the risk sits in the transfer itself, which is where my last two posts went.
A checklist before you trust a ratio
If you keep a balance on an exchange
- [ ] Find the latest report and note the snapshot time as well as the publication date.
- [ ] Check your own inclusion with the exchange's proof file or validator.
- [ ] Look at the ratio for each asset you actually hold, not only the headline figure.
- [ ] Compare the last few reports. A falling trend tells you more than a single number.
- [ ] Read the incident history and the size of any protection fund next to the ratio.
If you build on exchange APIs
- [ ] Show users when reserves were last proven, with the snapshot date.
- [ ] Treat withdrawal pauses as a normal state in your app, with clear messaging.
- [ ] Track where each venue is licensed or restricted, per user region.
- [ ] Don't present a reserve ratio as a safety rating. It is one input.
FAQ
Does a proof of reserves report mean the exchange was audited?
No. Most reports are attestations or self-published proofs, not financial audits. An audit looks at the whole company: all liabilities, internal controls, related parties and accounting policies. A proof of reserves covers a defined set of customer balances and wallets at one moment. When Mazars paused this work in December 2022, it stressed that its reports followed agreed-upon procedures and did not give an audit opinion. A report backs one specific claim: at the snapshot time, customer balances in scope were matched by on-chain assets.
Why would a reserve ratio be above 100% after a hack?
The ratio compares what the exchange holds with what it owes customers, and both sides move after an incident. If the exchange covers stolen funds from its own capital or a protection fund, customer balances stay whole while the company's own money shrinks. Many users also withdraw after a hack, which lowers liabilities and assets together. Bitget's 131% on September 29 reflected all of that. The incident still cost the company hundreds of millions; the ratio just shows customers were covered at the snapshot.
How can I verify my own balance was included?
Log in to the exchange and open its proof of reserves page, which usually links a per-user proof file for each report. Download the file for the latest report and run it through the verification tool the exchange provides, ideally an open-source one you can inspect. For Merkle tree reports, the tool rebuilds the path from your leaf to the published root. For zk-based reports, it checks the proof against the published commitment. If your balance isn't there, contact support and keep a screenshot of the report date.
Closing thoughts
Most of what an exchange tells you about itself has to be taken on trust. A reserve report is one of the rare exceptions, because you can run the check yourself. I'd still read it next to the snapshot date, your own inclusion proof and the incident history, since the ratio alone covers one moment and one kind of risk.
Do you check your inclusion proof when a new report comes out, or has anyone built that check into an app? I'd like to hear how others handle it.
Top comments (0)