BANKRUN.URL
VAULT.EXE
Redemption rate this epochEPOCH 0
Redemption rate this epoch: ----.----%
Vault remaining0.0000 / 0.0000 WETH
Vault balance
0.0000 WETH
Backing per RUN
0.00000000 WETH
Withdrawn this epoch
0.0000 WETH
Epoch closes in
--:--:--
Held: 0.00
You receive0.0000 WETH
Minimum accepted0.0000 WETH

Terminal is not connected to a vault.

No account authorised
CURVE.EXE
Rate against vault drainedALPHA 1.6
0%50%100% drained
Vault drained
--
Rate here
--
Your exit lands at
no balance
Still redeemable
0.0000 WETH
TELLER.EXE
A bank teller standing at a counter with their hands raised

Customer Services

This counter is not yet in service. No vault has been opened.

Your certificates remain valid and may be presented at any time during business hours. Business hours are continuous.

Correspondence
README.TXT

BANK RUN

BANK RUN is a Uniswap v4 hook game on Ethereum.

A pool has a vault. Every swap through the pool skims a fixed cut into the vault. Holders of the pool's token, RUN, can redeem against that vault instead of selling on the curve.

The vault pays out to people who exit. The payout rate collapses as the vault drains.

First out gets roughly the full backing. Halfway through the vault, roughly half. Last out gets nothing.

That is the whole game. Everyone can see the vault, everyone can see how much of it has been taken, and that information does not save them.

The mechanic

Each epoch, the contract takes two snapshots: the WETH sitting in the vault, and the total supply of RUN. Those two numbers fix the backing for the whole epoch:

backing = epochStartVault / totalSupply

The rate then decays with the fraction of that epoch's vault already withdrawn:

r       = (1 - drained / epochStartVault) ^ ALPHA
payout  = amount * backing * r
drained = drained + payout

ALPHA is fixed at deployment. It is 1.6. Higher values punish the queue harder; lower values flatten it. It cannot be changed by anyone, ever.

At ALPHA = 1.6, the curve looks like this:

Vault drained Rate you are paid
0% 100.00%
10% 85.87%
25% 63.11%
50% 32.99%
75% 10.88%
90% 2.51%
100% 0.00%

Why the rate decays on vault fraction, not headcount

The obvious version of this game charges you more the later you are in the queue — the tenth redeemer pays more than the first. That version is broken. You split your bag across ten wallets and you are the first redeemer ten times.

So the rate here depends on exactly one thing: how much of the vault has already been taken. Not who took it. Not how many transactions it took. Not how many addresses were involved. There is no per-address counter and no per-transaction counter anywhere in the contract, by design.

The consequence is that splitting is not merely useless, it is strictly worse. Every redemption moves drained up before the next one is quoted, so the second half of a split is always priced at a lower rate than the whole would have been. And because every division in the contract rounds down, floor(a + b) >= floor(a) + floor(b) means the arithmetic itself leans the same way.

This is tested, not asserted: the test suite fuzzes redemptions split into up to forty slices and checks that the total never beats doing it in one go.

Epochs

Epochs are 24 hours, fixed at deployment.

At each boundary:

  • drained resets to zero
  • epochStartVault re-snapshots to whatever WETH is actually in the vault
  • the RUN supply snapshot is re-taken

The vault refills from swap fees continuously, but fees that arrive during an epoch are not redeemable until the next one. The epoch's allowance is fixed at the boundary. This is deliberate: it means you cannot swap into the pool and redeem the fee you just generated in the same transaction.

Epoch boundaries sit on a fixed grid from deployment. If nobody touches the contract for a week, the next call rolls forward all seven epochs at once and lands on the same grid. Nothing can happen during a skipped epoch, so collapsing them is exact rather than an approximation.

Holders who didn't redeem are better off after an epoch where others did: tokens were burnt, and the redeemers who exited late took less than their proportional share of the vault with them. The backing per surviving token goes up.

The fee skim

The hook has exactly two permissions: afterSwap and afterSwapReturnDelta. It has no others, and the permission bits are encoded in its address, so this is verifiable without reading the code.

On each swap it takes 100 basis points — 1% — of the swap's unspecified currency and forwards it to the vault in the same call. The hook never holds a balance between calls.

One consequence worth stating plainly: Uniswap v4's return-delta mechanism can only adjust the unspecified currency of a swap, and the vault is WETH-denominated. So the skim only applies when the unspecified currency is WETH. In practice that means:

Swap Unspecified currency Skimmed
Sell RUN, exact input WETH (output) Yes
Buy RUN, exact output WETH (input) Yes
Buy RUN, exact input RUN (output) No
Sell RUN, exact output RUN (input) No

The sell side is taxed. That is a consequence of the vault being single-asset, not a behavioural design goal, but it does mean the vault fills fastest exactly when people are leaving.

If the vault is at its deposit cap, the skim is reduced to whatever still fits, possibly zero. It never reverts the swap and never strands tokens in the hook.


Contracts

What is deployed

Contract Purpose
RunToken The deposit certificate. Fixed supply, burnable, no mint function
Vault Holds WETH. The only way out is redeem
BankRunHook Uniswap v4 hook. Skims swap fees into the vault

Addresses are listed in the deployment section below.

What the vault cannot do

These are properties of the deployed bytecode, not promises:

  • No admin withdrawal. There is exactly one function that moves WETH out of the vault: redeem. There is no sweep, no rescue, no emergencyWithdraw, no withdraw.
  • No owner. No Ownable, no roles, no access-controlled function of any kind. There is no owner(), no transferOwnership, no pause.
  • No upgrade path. No proxy, no delegatecall, no implementation slot. The bytecode at the address is the whole system and it cannot be replaced.
  • No privileged redemption. The deployer is quoted at the same rate as everyone else and has no ordering advantage.
  • No new supply. RunToken mints once, in its constructor. There is no mint function afterwards. Supply only ever goes down, through redemption burns.

Every parameter — ALPHA, the epoch length, the fee, the deposit cap, the token address, the WETH address — is immutable or constant. Nothing about this system can be tuned after deployment. If something turns out to be set wrong, it stays wrong.

The deploy script asserts none of the usual escape-hatch selectors resolve on the deployed contracts before it finishes, and the test suite asserts the same thing against a live vault.

The deposit cap

The vault has an immutable ceiling on how much WETH it will accept through deposit. This is unaudited code holding real money; the cap bounds the blast radius.

When the vault is at its cap, the hook skims whatever still fits — possibly nothing — rather than reverting. Swaps keep working. The cap does not lock anything: WETH already in the vault is redeemable on exactly the same terms as before.

The cap applies to the deposit path. WETH transferred directly to the vault address, bypassing deposit, is not rejected and simply becomes part of the next epoch's backing. It cannot be recovered by anyone, including whoever sent it. Do not send WETH directly to the vault.

Interface

// Views
function currentRate() view returns (uint256);        // 1e18 fixed point
function quoteRedeem(uint256 amount) view returns (uint256);
function backing() view returns (uint256);
function remainingAllowance() view returns (uint256);
function vaultBalance() view returns (uint256);
function epochEndsAt() view returns (uint256);
function snapshot() view returns (...);               // everything, in one call

// State
function redeem(uint256 amount, uint256 minOut) returns (uint256 out);
function deposit(uint256 amount);                     // permissionless top-up
function poke();                                      // roll the epoch, idempotent

Every view rolls the epoch forward in memory before answering, so a quote is never stale just because nobody has poked the contract since the boundary.

redeem requires an ERC-20 allowance to the vault; it burns from the caller with burnFrom.

minOut

redeem takes a minOut. Use it. The rate you were quoted is the rate at the drained value you saw, and anyone landing in front of you moves it down. A redemption that would pay less than minOut reverts rather than filling at whatever is left.

This is the one piece of protection the contract offers, and it protects you from the exact thing the game is about.

Events

event Redeemed(
    address indexed account,
    uint256 indexed epoch,
    uint256 amount,
    uint256 payout,
    uint256 rate,
    uint256 drainedAfter,
    uint256 epochStartVault
);

event EpochRolled(uint256 indexed epoch, uint256 epochStart, uint256 epochStartVault, uint256 supplySnapshot);
event Deposited(address indexed from, uint256 amount, uint256 balanceAfter);

The front end watches Redeemed and spawns a dialog for every one of them. Those dialogs are driven by real chain events, not decoration.

Rounding

Every division in the payout path floors. A redeemer can be paid less than the exact real-valued result but never more.

The exponent is evaluated through an exp/ln approximation accurate to a few wei, which is not provably monotone at one-wei input granularity. So the remaining-vault fraction is snapped down to a 1e-8 grid before exponentiation. Within a grid step the rate is bit-identical; between steps the real difference is roughly ten orders of magnitude larger than the approximation error. This makes "the rate never goes back up within an epoch" a property of the code rather than a hope about a numerical library.

Quantising downward also preserves the round-down guarantee.

Solvency

The property that matters: drained can never exceed epochStartVault.

For ALPHA >= 1, the function x + (1-x)^ALPHA is at most 1 on [0, 1]. So even the worst possible single step — one holder with the entire snapshot supply exiting from an untouched vault — lands exactly on the vault and never past it. The constructor rejects any ALPHA below 1e18 for this reason.

A redundant clamp in the payout function enforces the same bound directly, so the invariant does not rest on the analysis alone.

The test suite checks this as an invariant across randomised sequences of deposits, redemptions and time travel, and separately as a bounded fuzz over the arithmetic in isolation.


Deployment

Addresses

Sepolia

Contract Address
RunToken not yet deployed
Vault not yet deployed
BankRunHook not yet deployed
PoolManager (v4) 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543
WETH 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14

Ethereum mainnet

Contract Address
RunToken not deployed
Vault not deployed
BankRunHook not deployed
PoolManager (v4) 0x000000000004444c5dc75cB358380D2e3dE08A90
WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2

This document is the source of truth for addresses. If a contract is listed as not deployed, it is not deployed, and anything claiming to be it is not it.

Parameters at deployment

Parameter Value Changeable
ALPHA 1.6e18 Never
Epoch length 24 hours Never
Fee skim 100 bps (1%) Never
Deposit cap 100 WETH Never
RUN supply 1,000,000 Never

The hook address

Uniswap v4 reads a hook's permissions from the low 14 bits of its address. BankRunHook needs exactly two bits set — afterSwap (bit 6) and afterSwapReturnDelta (bit 2) — which means the deployed address must end in bits 0b00000001000100.

You cannot pick that address; you have to mine for it. The deploy script does this with HookMiner, searching CREATE2 salts against the canonical deterministic deployer at 0x4e59b44847b379578588920cA78FbF26c0B4956C until the resulting address has the right bits, then deploys with that salt.

The hook's constructor independently checks its own address and reverts if the bits are wrong, so a mis-mined deployment fails immediately rather than producing a hook the PoolManager will silently refuse to call.

Verifying a deployment yourself

Do not take the addresses above on trust. Check:

  1. The hook's permission bits. uint160(hookAddress) & 0x3FFF must equal 0x44. Any other value means it can do things this document does not describe.
  2. The vault has no owner. cast call $VAULT "owner()" must revert. So must pause(), sweep(address,uint256), emergencyWithdraw() and upgradeTo(address).
  3. The vault's immutables. ALPHA(), DEPOSIT_CAP(), EPOCH_LENGTH(), weth() and token() should match the table above.
  4. The token has no mint. cast call $TOKEN "mint(address,uint256)" $YOU 1 must revert, and totalSupply() should only ever decrease from here.
  5. Verified source. The contracts are verified on the block explorer. Read them.

Reproducing the build

cd contracts
forge build
forge test -vvv

The fork tests run against a live mainnet RPC. Set MAINNET_RPC_URL to use your own endpoint; they fall back to a public one and skip cleanly if no endpoint is reachable.


Risk

Read this part.

What this is

This is a game about a bank run. It is designed so that the people who exit early take money from the people who exit late. That is not a side effect or a failure mode. That is the product.

If you buy RUN and you are not the first out, you will get back less than the backing you paid for. If you are last out, you get nothing from the vault. The contract will do this correctly and without complaint.

There is no scenario in which everyone gets their money back. The arithmetic does not permit it. A vault of V backing a supply of S pays out strictly less than V for any sequence of redemptions other than one holder with the entire supply exiting first.

What could go wrong

The code is unaudited. It has 81 tests including invariant and fork tests, and it has not been reviewed by anyone who does this professionally. Tests find the bugs you thought of. An audit finds the ones you didn't. Treat everything below as conditional on there being a bug you and I both missed.

The vault has a deposit cap for exactly this reason. It bounds how much can be lost to a contract bug. It does not bound how much can be lost to the game working as intended.

Nothing can be fixed. There is no owner, no pause, no upgrade path. If a bug is found in the deployed contracts, there is no mechanism to stop it, patch it, or recover funds. The same property that means nobody can rug you means nobody can save you. This is a deliberate trade and you should decide whether you agree with it before you participate.

The exponent uses a fixed-point exp/ln approximation. It is accurate to a few wei and the contract quantises its input to make monotonicity robust, but it is an approximation, not exact arithmetic.

Uniswap v4 hooks are new. The hook here is small and does one thing, but the surrounding machinery is young and hook-adjacent bugs have been found in other systems.

The token has no value floor other than the vault. The vault is funded by swap fees. If nobody swaps, the vault does not grow, and the backing stays wherever it is.

You are competing with people who have better tooling than you. Redemptions are transparent and priced by ordering. Anyone running a bot will see the vault state and act on it before you do. The rate you are quoted in a UI is the rate at the block you read it; by the time your transaction lands, someone may have moved it. Use minOut.

Front-running is not a bug here, it is the mechanic. There is no mempool protection, no commit-reveal, no batching. If you are unhappy about being front-run out of a payout, this game is not for you, and no version of it would be.

What this is not

It is not an investment. It is not a yield product. It is not a stablecoin, a treasury, or a protocol with a roadmap. Nothing here accrues to anyone but whoever gets to the vault first.

There is no team allocation described anywhere in this document because there is no mechanism in the contracts to create one after deployment — but the initial supply is minted to a single recipient at deployment, and what that recipient does with it is not constrained by code. Check the deployment transaction and the token holder distribution yourself before deciding that means anything.

The short version

You can lose everything you put in. The most likely way you lose is not a hack — it is being slower than someone else at a game that is explicitly about being fast. Everyone can see the vault, everyone can see the drain, and that information does not save them.

Do not put in money you need.