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 token | Mintable twin | |
|---|---|---|
| Minted by BTR | no | yes (TestnetERC20) |
| Faucet-claimable | no | yes, subject to an on-chain cap |
| Seeded to | its per-asset seedUsd | its 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
- Open
/faucet.?token=USDCdeep-links one asset to the top of the list. - First visit only: press Enable faucet. The app sends
register(), which setswhitelisted[msg.sender] = true. - 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:
NotWhitelistedif you never registered;NoCapifdailyCap[token] == 0;CapExhaustedif 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.
| Parameter | Value | Source |
|---|---|---|
FAUCET_CAP_DIVISOR | 50 | deploy script |
FAUCET_PREFUND_CLAIMS | 200 | deploy script |
seedUsdMinted | USD seeded per mintable leg | the instance’s deploy manifest |
seedUsd (per asset) | USD seeded per un-mintable issuer token, a much smaller figure | the 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’ssetWhitelisted(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 revertsNoCap. ReaddailyCapdirectly 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
safeTransferwith no dedicated error. Onlyfund()refills it.
7. Related documentation
- Pool Legs: what a listing is, the base leg, and why the hub is a twin
- Asset Registry: per-asset addresses, feeds, ERC-20 oddities
- Contract Addresses: the
TestnetFaucetaddress per chain