r"""
Flash loans and oracle manipulation: the bZx attacks (2020)
===========================================================

A lender must know what its collateral is worth, and on a blockchain the
easiest answer is the spot price of a decentralized exchange. In February
2020, attackers took flash loans, used them to move such a price within a
single transaction, and borrowed from the bZx protocol against collateral
it then overvalued, walking away with the difference.

The mechanism fits in one inequality. Borrowing :math:`U` and buying the
collateral token from a pool with reserves :math:`(x, y)` raises its spot
price to the point where the tokens bought are valued at
:math:`U (y + U) / y`. A lender requiring 150% collateral then lends

.. math::

   \frac{U (y + U)}{1.5\, y} > U \quad \text{whenever} \quad U > \frac{y}{2},

so the flash loan is repaid from the new loan, with a profit, and the
lender keeps collateral worth far less than it lent. A price recorded
*before* the transaction, the idea behind time-weighted averages, cannot
be moved by a flash loan.
"""

# %%
import matplotlib.pyplot as plt

import blockchainkit as bk
from blockchainkit.contracts.visualizers import plot_call_tree

# %%
# A pool, a lender, and a flash lender
# ------------------------------------


def stage(delayed, loan=300_000):
    world = bk.contracts.World()
    tok = world.deploy("dex", bk.contracts.ERC20, 10**6, name="TOK")
    usd = world.deploy("bank", bk.contracts.ERC20, 10**7, name="USD")
    world.transact("bank", usd, "transfer", "dex", 100_000)
    pool = world.deploy("dex", bk.economics.ConstantProductPool, tok, usd, name="pool")
    for token in (tok, usd):
        world.transact("dex", token, "approve", pool, 10**6)
    world.transact("dex", pool, "add_liquidity", 100_000, 100_000)  # TOK at 1 USD.
    lender = world.deploy("bank", bk.economics.OracleLender, tok, usd, pool, delayed, name="lender")
    world.transact("bank", usd, "transfer", lender, 2_000_000)
    flash = world.deploy("bank", bk.contracts.FlashLender, usd, name="flash lender")
    world.transact("bank", usd, "transfer", flash, 2_000_000)
    world.advance()
    attacker = world.deploy("eve", bk.economics.OracleAttacker, name="attack contract")
    receipt = world.transact("eve", attacker, "attack", flash, pool, lender, loan)
    return world, usd, lender, receipt


world, usd, lender, attack = stage(delayed=False)
profit = world.view(usd, "balance_of", "eve")
print(f"spot-price oracle: {attack.success}, attacker keeps {profit:,} USD")
assert attack.success and profit > 400_000

safe_world, _, _, defended = stage(delayed=True)
print("price from before the block:", defended.error)
assert defended.error == "undercollateralized"

fig, ax = plt.subplots(figsize=(10, 6))
plot_call_tree(attack, names=world.name, ax=ax)
ax.set_title("One transaction: borrow, pump, over-borrow, repay. " + ax.get_title())
fig.tight_layout()

# %%
# Profit against the size of the flash loan
# -----------------------------------------

loans = [25_000 * i for i in range(1, 17)]
profits = []
for loan in loans:
    w, token, _, receipt = stage(delayed=False, loan=loan)
    profits.append(w.view(token, "balance_of", "eve") if receipt.success else 0)
predicted = [max(0, u * (100_000 + u) / 150_000 - u) for u in loans]
assert profits[0] == 0 and profits[-1] > 0  # Small loans do not pay for themselves.

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.plot(loans, profits, "o", color="#dc2626", label="simulated (0.3% fee)")
ax.plot(loans, predicted, color="black", label="U (y + U) / 1.5 y - U, no fee")
ax.axvline(50_000, color="#64748b", linestyle=":", label="U = y / 2")
ax.set(xlabel="flash loan U (USD)", ylabel="attacker profit (USD)")
ax.set_title("A deeper pool makes the attack costlier")
ax.legend()
fig.tight_layout()

plt.show()

# %%
# Exercise
# --------
# Double the pool's reserves. How large must the flash loan now be for the
# attack to pay, and how much would it have to cost to borrow for a
# profitable attack to remain?
