Protocol · BitscallVault · ERC-4626

Discover. Allocate. Deploy.

One USDC vault on Base. It scores each venue by the yield it has actually realized onchain, turns scores into capped target weights, and moves capital toward them within hard per-epoch limits. Three permissionless calls, once per day, in order.

01

The vault

BitscallVault is an ERC-4626 vault whose asset is native USDC on Base (0x8335…2913). Depositing mints bcUSDC shares; the share price rises as the venues underneath pay yield. The vault uses a decimals offset of 6, which makes first-depositor inflation attacks uneconomical.

Capital sits in two places: the buffer — USDC held directly by the vault, targeted at 5% of assets — and up to eight venues, each an ERC-4626 vault whose asset is also USDC.

totalAssets() = buffer + Σ venue.convertToAssets(venue.balanceOf(vault))

At launch the board holds five venues: Gauntlet USDC Prime and Steakhouse USDC (Morpho), Spark USDC Vault, Fluid USD Coin and Moonwell Flagship USDC. Each was checked onchain to use USDC as its asset. Their live readings are on the board.

02

The cycle

Deposits can happen at any time. The allocation work happens in a 24-hour epoch, through three functions that anyone may call: discover(), then allocate(), then deploy(). Each runs at most once per epoch; calling them out of order, or twice, reverts.

The Bitscall cycle: deposit into the buffer, then discover, allocate and deploy once per epoch 1 · CAPITAL IN deposit() 2 · DISCOVER discover() 3 · ALLOCATE allocate() 4 · DEPLOY deploy() NEXT EPOCH · ≥ 24 H LATER reads share prices sets target weights moves the capital any time · into the buffer
Fig. 1 — The path from the X banner is the call order. Steps 2–4 are permissionless and gated by the epoch clock.
03

discover() — measure, don't ask

For every active venue, discover() reads the price of one whole share, pps = v.convertToAssets(10**v.decimals()), and compares it with the reading it stored last time.

apr = (pps − lastPps) × 365 days × 10⁴ / (lastPps × dt) // basis points apr = min(apr, 5000) // APR_CAP_BPS = 50%
  • The score is what the venue paid, not what it advertises. No oracle, no off-chain feed, no operator input.
  • If the share price went down, the venue is marked impaired and scores zero. It stays off the board until the owner reactivates it — and reactivation starts with a fresh reading.
  • The first reading of a venue only primes it (APR 0).
  • The 50% cap bounds what a donation to a venue can buy: at most its 35% target cap, never the whole vault.
04

allocate() — water-filling with caps

Scores become target weights on 95% of assets (1e4 − BUFFER_BPS). Each healthy venue gets a weight proportional to its APR, capped at its capBps (35% by default). Whatever a capped venue cannot take is redistributed to the others, pass after pass, bounded to eight passes. If every venue scores zero, nothing moves: the previous targets are kept. If the caps together cannot absorb 95%, the remainder simply stays in the buffer.

Water-filling: a venue whose proportional share exceeds the 35% cap is clipped and its excess is shared among the others CAP 35% PER VENUE PASS 1 · proportional to APR excess above cap PASS 2 · clip, redistribute + shared excess
Fig. 2 — Schematic, not live data. Weights sum to 95%; the buffer keeps the rest. The live version of this computation runs on /board.
05

deploy() — move, within limits

  1. Retired or impaired venues are exited in full, bounded only by what they let the vault withdraw (maxWithdraw). This is the safety exit and has no rate limit.
  2. Venues above target are trimmed back to the buffer. The total of these rebalancing withdrawals is capped at MAX_MOVE_BPS = 1000 — 10% of totalAssets — per epoch. The budget is tracked across calls, so splitting the work into several calls does not raise it.
  3. Buffer above 5% is deposited into venues below target. Idle capital going to work is not rate limited.

Every step emits an event — Discovered, Allocated, Deployed(venue, delta) — so the whole history of the vault can be replayed from logs.

Rebalancing budget: 10 percent of assets per epoch, shared by every deploy call in that epoch REBALANCING BUDGET · ONE EPOCH · 10% OF totalAssets 1st deploy() what is left A second call in the same epoch can only spend what is left of the same budget. budget spent · further trims wait for the next epoch cap · per epoch, not per call
Fig. 3 — Schematic. Exits from impaired or retired venues, and deposits of idle buffer, do not consume this budget.
06

Withdrawals

A withdrawal is paid from the buffer first, then from venues in order of increasing score — the weakest venue is drawn down first — each up to its own maxWithdraw. The vault overrides maxWithdraw and maxRedeem to report the liquidity that is really available (buffer plus what each venue will release), so a request larger than that reverts cleanly instead of half-executing.

07

Fees

There is one fee: a 10% performance fee on the rise of the share price above its high-water mark. It is taken by minting bcUSDC shares to the treasury whenever the vault accrues (before deposits, withdrawals and deploys). After a loss, no fee is charged until the share price has recovered past its previous high. The owner can only lower this fee. There is no management fee and no entry or exit fee.

Fee flows: vault performance fee and the token pool hook toll both go to the Bitscall treasury BitscallVault gain above high-water mark Uniswap v4 · ETH pool hook toll on swaps BitscallTreasury immutable recipient 10% · minted as bcUSDC toll · per swap
Fig. 4 — Both revenue streams of the protocol end in the same treasury. The token side is described on /token.
08

Governance

The owner is the deployer. Its powers are listed exhaustively:

Propose a venue (proposeVenue), must use USDC as asset
—
Add a proposed venue (addVenue)
after 48 h
Retire a venue (retireVenue), or exit it at once (exitVenue)
immediate
Reactivate an impaired venue (reactivate)
re-primes
Set a venue cap (setCap)
≤ 50%
Set the deposit cap (setDepositCap)
launch: 250,000 USDC
Lower the performance fee
down only

The contract has no arbitrary-call function, no rescue of USDC, and no path that sends funds anywhere other than whitelisted venues, the depositors, and fee shares to the treasury.

09

Threat model

Each of these is a launch requirement of the contract, with its own test:

  • First-deposit inflation — neutralised by the 6-decimal offset.
  • Donation to a venue before discover() — APR capped at 50%, weight capped at 35%.
  • Calls out of order or twice per epoch — revert.
  • Splitting deploy() to dodge the rebalancing cap — the cap is per epoch.
  • Illiquid venue — honest maxWithdraw, clean revert on an unpayable withdrawal.
  • Venue loss — impaired, exited, and no performance fee on the recovery.