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.
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.
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.
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.
- 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.
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.
deploy() — move, within limits
- 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. - Venues above target are trimmed back to the buffer. The total of these rebalancing withdrawals is capped at
MAX_MOVE_BPS = 1000— 10% oftotalAssets— per epoch. The budget is tracked across calls, so splitting the work into several calls does not raise it. - 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.
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.
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.
Governance
The owner is the deployer. Its powers are listed exhaustively:
proposeVenue), must use USDC as assetaddVenue)retireVenue), or exit it at once (exitVenue)reactivate)setCap)setDepositCap)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.
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.