DEV Community

Huzaifa Zahoor
Huzaifa Zahoor

Posted on

What Failed Crypto Exchanges Teach Developers About Custody Risk (Plus a Merkle Proof Check in Python)

Every few years a crypto exchange stops processing withdrawals, and a lot of people learn the same lesson the hard way: a balance on an exchange is a number in someone else's database, not coins you control.

Mt. Gox, QuadrigaCX, Cryptopia, Thodex, FTX and Bittrex's US business all failed for different reasons. Read their histories side by side, though, and the failure modes start to look like bugs in a system design. That's the angle of this post: what went wrong, why it matters if you build or use crypto products, and one small verification you can actually run.

For the full list of collapses, dates and recovery figures, see this list of failed crypto exchanges and what customers got back.

The failures were mostly not about price

It's tempting to think exchanges die in crashes. Most of the big ones didn't. They failed because of:

  • Theft. Mt. Gox lost around 850,000 BTC (about 200,000 were later found). Cryptopia lost roughly a tenth of its assets in a 2019 hack.
  • Insider misuse of funds. Canada's securities regulator found QuadrigaCX's collapse came from its founder's fraud, with fake accounts and made-up balances. At FTX, an $8 billion hole appeared once withdrawals spiked.
  • Regulators or law enforcement stepping in. BTC-e was seized by US authorities. Bittrex shut its US business weeks after an SEC complaint.

In engineering terms: the single source of truth (the exchange's internal ledger) diverged from reality, and nobody outside could see it until withdrawals stopped.

Withdrawals are the health check

In almost every case, the first public symptom was paused or slow withdrawals, usually explained as "technical issues" or "market conditions." If you run anything that holds funds on an exchange, such as a trading bot, a treasury, or a payment flow, it's worth treating withdrawal latency as a metric. Alert on it the way you'd alert on API error rates.

Recovery is also slow. Mt. Gox creditors waited about a decade, Cryptopia users nearly six years, and FTX customers more than two. FTX paid claims in US dollars at November 2022 prices, so people got dollars back, not the coins they'd held. In the US, FDIC deposit insurance doesn't cover crypto at all.

Proof of reserves: what you can verify yourself

After FTX, many exchanges started publishing "proof of reserves," often with a Merkle tree of customer balances. The idea: the exchange hashes every account balance into a tree and publishes the root. You get a proof showing your leaf is part of that tree.

Here's a minimal, self-contained version of that inclusion check:

import hashlib

def h(data: bytes) -> bytes:
    return hashlib.sha256(data).digest()

def leaf(account_hash: str, balance_sats: int) -> bytes:
    return h(f"{account_hash}:{balance_sats}".encode())

def build_tree(leaves):
    levels = [leaves]
    while len(levels[-1]) > 1:
        cur = levels[-1]
        if len(cur) % 2:
            cur = cur + [cur[-1]]  # duplicate last node on odd levels
        levels.append([h(cur[i] + cur[i + 1]) for i in range(0, len(cur), 2)])
    return levels

def proof_for(levels, index):
    proof = []
    for level in levels[:-1]:
        if len(level) % 2:
            level = level + [level[-1]]
        sib = index ^ 1
        proof.append((level[sib], "left" if sib < index else "right"))
        index //= 2
    return proof

def verify(my_leaf, proof, published_root):
    node = my_leaf
    for sibling, side in proof:
        node = h(sibling + node) if side == "left" else h(node + sibling)
    return node == published_root

if __name__ == "__main__":
    accounts = [("a1f3", 50_000), ("b7c2", 1_200_000), ("c9d0", 75_000),
                ("me42", 310_000), ("e5aa", 9_000)]
    levels = build_tree([leaf(a, b) for a, b in accounts])
    root = levels[-1][0]
    proof = proof_for(levels, 3)
    print("my balance included:", verify(leaf("me42", 310_000), proof, root))
    print("tampered balance included:", verify(leaf("me42", 31_000), proof, root))
Enter fullscreen mode Exit fullscreen mode

Running it prints True for the real balance and False for a tampered one. Real exchanges use their own leaf formats (often with salts and per-asset balances), so read their docs before adapting this.

Know the limits, too. An inclusion proof shows your balance was counted. It doesn't prove the exchange has no hidden debts, that the reserve wallets weren't borrowed for the snapshot, or that things are still fine a week later. It's a useful signal, not a guarantee.

Lowering your own exposure

The original guide's advice translates well to how developers think about risk:

  • Minimize blast radius. Keep only what you're actively trading on an exchange. Long-term holdings belong in self-custody, and moving funds to a cold wallet is a one-time setup.
  • Prefer regulated venues. Licensing adds custody and disclosure rules. It doesn't remove risk.
  • Question yield. Celsius and Voyager paid interest by lending out customer crypto. If a return appears, ask where it comes from.
  • Keep your own records. Export statements and trade history regularly. If a platform fails, you'll need them for a claim and for taxes.

Takeaways

  • Exchange balances are IOUs; the ledger can diverge from reality.
  • Slow withdrawals have preceded nearly every major collapse, so monitor them.
  • Merkle proofs of reserves are verifiable, but they only prove inclusion.
  • Recovery usually takes years and may be paid in dollars, not coins.
  • Self-custody and good record-keeping shrink your exposure.

Read the full guide on NutshellCrypto

This is educational content, not financial or legal advice.

This post was written with AI assistance.

Top comments (0)