Faucets

A faucet exists only on a testnet deployment, and only for that instance’s mintable mock tokens. Which instances are testnets: deployed instances. Testnet assets have no value, and a faucet hands out pool assets, never gas (Pool Legs §5).


1. What the faucet is

TestnetFaucet is a rate-limited dispenser, not a minter. The deploy ceremony mints to the deployer, approves the faucet, calls fund(token, amount) with FAUCET_PREFUND_CLAIMS days of cap, then setCap(token, cap). claim is a safeTransfer out of that pre-funded balance. Minting itself is minter-only on TestnetERC20, and there is no setter for the minter, so an empty faucet is refilled only by fund().

2. Issuer tokens versus mintable twins

A testnet roster mixes two kinds of token. Some are the official issuer deployments, declared in the deploy manifest’s tokens map and never minted by BTR; the rest are BTR mocks, new TestnetERC20(sym + ".b", sym + ".b", 18).

Official issuer tokenMintable twin
Minted by BTRnoyes (TestnetERC20)
Faucet-claimablenoyes, subject to an on-chain cap
Seeded toits per-asset seedUsdits per-asset seedUsd, and topped up by the liquidity keeper

The ceremony classifies a token by staticcalling minter(): an issuer token reverts and is treated as un-mintable float. That is also why the hub leg is a twin rather than the issuer token (Pool Legs §2). Per-instance addresses and decimals for both kinds: Asset Registry.

2.1. .b is display metadata, never a key

The .b lives in the ERC-20 name()/symbol() and nowhere else. The roster key is dot-free (USDCB), with an explicit display: 'USDC.b' override for rendering. The SDK’s canonicalTokenSymbol strips a trailing .b, so getToken('USDC.b') resolves to the official 6-decimal issuer token and getToken('USDCB') to the 18-decimal twin. Feeding a rendered symbol back into a lookup silently swaps one asset for the other; carry the key, not the label.

2.2. A twin owns no feed of its own

Twins quote off a feed the keeper already services, and sit a few basis points off the issuer token as a result. Which feed each twin uses, the ref band it carries and the consequences of the shared feed: Asset Registry, faucet twins.

3. Claiming

  1. Open /faucet. ?token=USDC deep-links one asset to the top of the list.
  2. First visit only: press Enable faucet. The app sends register(), which sets whitelisted[msg.sender] = true.
  3. Press Claim on a row. The app sends claim(address token) and re-reads whitelist and remaining allowance from the receipt.

The button is live only when the faucet is deployed, you are whitelisted, and remaining(user, token) > 0. When you are disconnected or on another network the same click connects or switches chain instead. Reads are pinned to the deployment’s chain id regardless of the wallet’s current network.

4. What claim enforces

claim reverts:

  • NotWhitelisted if you never registered;
  • NoCap if dailyCap[token] == 0;
  • CapExhausted if the day’s allowance is spent.

Otherwise it resets the day bucket if the day rolled, transfers the full remaining amount, and writes claimedToday = cap.

  • One claim per token per day, all-or-nothing: the write is claimedToday = cap, not an increment, so there is no partial second claim.
  • The day is block.timestamp / 1 days, a UTC-midnight roll rather than 24 hours since your last claim. Claim at 23:55 UTC and you can claim again five minutes later.

5. Caps

The cap is derived, not typed: a leg’s USD depth target divided by FAUCET_CAP_DIVISOR (2% of the target per day), converted at the ceremony mark into the token’s own decimals.

ParameterValueSource
FAUCET_CAP_DIVISOR50deploy script
FAUCET_PREFUND_CLAIMS200deploy script
seedUsdMintedUSD seeded per mintable legthe instance’s deploy manifest
seedUsd (per asset)USD seeded per un-mintable issuer token, a much smaller figurethe instance’s deploy manifest

A 50,000 USD seed, for example, gives roughly 1,000 USD per mintable leg per day, and 200 days of cap are placed in the faucet at deploy.

dailyCap(token) read on chain is the enforced limit. The per-token daily figures in the app’s config are a display fallback used only when that read is missing or zero, and several of them are stale relative to the derived cap.

6. Limits

  • register() is permissionless on chain; the app adds no balance check. The on-chain backstops are the daily caps and the owner’s setWhitelisted(user, false) revoke.
  • Issuer tokens are never claimable: excluded from the list and skipped by the deploy’s funding step. Where the chain’s gas token is one of them, a brand-new wallet cannot bootstrap gas from the faucet and must arrive already funded.
  • An uncapped token looks identical to an exhausted one. With dailyCap == 0, remaining() returns 0 and the row reads Done today; a claim in that state reverts NoCap. Read dailyCap directly to tell the two apart; a token listed out of band should have its cap verified on chain rather than assumed.
  • An empty faucet reverts inside safeTransfer with no dedicated error. Only fund() refills it.