Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
FTX, commingled customer funds, and proof of liabilities (2022)#
In November 2022 FTX, then one of the largest exchanges, could not meet a rush of withdrawals and filed for bankruptcy. Its affiliated trading firm, Alameda Research, had been allowed a negative balance on the exchange and had spent billions of dollars of customer deposits. The books still showed every customer’s balance; the coins were gone. With reserves \(R\) and liabilities \(L\), a run pays the first customers in full and the rest nothing once
A proof of reserves alone could not have shown this, since reserves were real and large; only reserves set against a proof of the liabilities can. Exchanges began publishing Merkle-sum-tree proofs of liabilities weeks after the collapse.
import matplotlib.pyplot as plt
import blockchainkit as bk
An exchange lends deposits to its affiliate#
world = bk.contracts.World()
customers = [f"customer{k:02}" for k in range(20)]
for name in customers:
world.fund(name, 1_000)
exchange = world.deploy("operator", bk.fraud.Custodian, "affiliate", 15_000, name="exchange")
for name in customers:
world.transact(name, exchange, "deposit", value=1_000)
print("before:", world.view(exchange, "reserves"), "held for", world.view(exchange, "liabilities"))
world.transact("operator", exchange, "lend_to_affiliate", 12_000)
reserves, liabilities = world.view(exchange, "reserves"), world.view(exchange, "liabilities")
print(f"after: reserves {reserves:,}, owed {liabilities:,}")
before: 20000 held for 20000
after: reserves 8,000, owed 20,000
Reserves against a proof of liabilities#
tree = bk.fraud.liability_tree(world.view(exchange, "balances"), salt=b"2022-11")
ratio = bk.fraud.reserve_ratio(reserves, tree.total)
print(f"proof of liabilities: total {tree.total:,}, reserve ratio {ratio:.0%}")
assert tree.total == liabilities and ratio == 0.4
proof of liabilities: total 20,000, reserve ratio 40%
The run#
results = [world.transact(name, exchange, "withdraw", 1_000) for name in customers]
paid = [r.success for r in results]
print("withdrawals paid:", sum(paid), "of", len(paid), "->", results[-1].error)
assert sum(paid) == ratio * len(customers)
fig, ax = plt.subplots(figsize=(8, 4))
ax.bar(
range(len(paid)),
[1_000 if ok else 0 for ok in paid],
color=["#16a34a" if ok else "#dc2626" for ok in paid],
)
ax.set(xlabel="order in the queue", ylabel="ether recovered")
ax.set_title(f"A run on an exchange with a reserve ratio of {ratio:.0%}")
fig.tight_layout()
plt.show()

withdrawals paid: 8 of 20 -> withdrawals paused
Exercise#
The affiliate returns its loan the day before a published proof of reserves, then borrows it again. Which checks pass, and what would a proof of reserves need to rule this out? A worked solution is in Exercises: fraud.
Total running time of the script: (0 minutes 0.034 seconds)