Step 1: Get DS Tokens
The first step is funding the investor wallet with testnet DS Token (DST) — minted from the project's faucet, which also auto-registers the wallet in the DS registry.
DS Token via the faucet
What actually happens on-chain
The faucet does not mint. Faucet.requestTokens(recipient) (in bc-securitize-faucet-sc):
- Checks
getRemainingCooldown(recipient) == 0. - Checks the provider wallet has enough allowance:
IERC20(token).allowance(providerWallet, address(faucet)) >= dripAmount. - If
registry.getInvestor(recipient)is empty, callsregistry.addWallet(recipient, newInvestorId)and emitsWalletAutoRegistered(recipient, investorId). - Calls
IERC20(token).transferFrom(providerWallet, recipient, dripAmount). - Emits
TokensRequested(recipient, dripAmount)and updateslastClaimTime[recipient].
The auto-registration in step 3 is the reason the next steps work — VaultRegistrar.registerVault() will fail with InvestorNotFound for any wallet that has not been seen by the registry.
Path A — Frontend UI
Visit /faucet in the sandbox frontend.
- Pick a network from the dropdown (default:
ethereum-sepolia). - Enter the investor wallet address (or auto-fill from the connected wallet).
- Click Request tokens.
- The page shows the tx hash linked to the appropriate block explorer.
The frontend posts to POST /api/v1/drip on bc-labs-gw. The gateway holds FAUCET_OPERATOR_KEY and submits the on-chain requestTokens(investor) for you.
Path B — Direct gateway call
curl -X POST "$LABS_GW_URL/api/v1/drip" \
-H "Content-Type: application/json" \
-d '{
"networkId": "ethereum-sepolia",
"recipient": "0xYourInvestorWallet"
}'
Successful response:
{ "success": true, "txHash": "0x…", "networkId": "ethereum-sepolia" }
Important: the gateway returns on submit, not on confirmation. Watch the tx hash on the block explorer if you need to wait for finality. See the gateway caveat below.
Path C — Direct on-chain (admin/operator only)
If you hold a faucet OPERATOR_ROLE key, call requestTokens(recipient) directly. Most external developers will not have this — use Path A or B instead. There is also requestTokensWithSignature(recipient, nonce, deadline, signature) for delegated submission, with a per-signer nonce tracked via OZ NoncesUpgradeable.
Cooldown, drip amount, and limits
| Property | Default in test fixture | How to read at runtime |
|---|---|---|
dripAmount | 100 * 10^6 (100 DST, 6 decimals) | faucet.dripAmount() |
cooldownPeriod | 86_400 seconds (24h) | faucet.cooldownPeriod() |
| Per-recipient cooldown | tracked per wallet | faucet.getRemainingCooldown(recipient) |
| Provider allowance left | drives InsufficientAllowance | faucet.getAvailableAllowance() |
| Per-IP rate limit (gateway) | 10 / 24h | enforced in bc-labs-gw RateLimitGuard |
| Per-wallet rate limit (gateway) | 3 / 24h | enforced in bc-labs-gw RateLimitGuard |
| Strike threshold (gateway) | 3 → 24h IP block | enforced in bc-labs-gw RateLimitGuard |
The contract-level cooldown and the gateway-level rate limit are independent. Hitting either one will block a request.
Supported networks
| Network | Chain ID | Faucet env var (gateway) | Faucet env var (frontend) |
|---|---|---|---|
| Ethereum Sepolia | 11155111 | FAUCET_SEPOLIA | VITE_FAUCET_SEPOLIA |
If the Sepolia env vars are unset on the gateway, the faucet flow will not initialize correctly. Watch the gateway boot logs to confirm the expected chain initialized.
Errors and how to recover
| Where | Error | Cause | Fix |
|---|---|---|---|
| Contract | CooldownNotElapsed(address,uint256) | Recipient claimed within cooldownPeriod | Wait until getRemainingCooldown(recipient) == 0 |
| Contract | InsufficientAllowance(uint256,uint256) | Provider wallet allowance to faucet too low | Operator must top up IERC20.approve(faucet, …) from the provider wallet |
| Contract | InvalidAddress() | Recipient is the zero address | Pass a valid address |
| Gateway | 403 Forbidden | IP blocklisted | Contact support if the IP is auto-blocked |
| Gateway | 429 Too Many Requests (with Retry-After) | Per-IP or per-wallet rate limit hit | Wait Retry-After seconds, then retry |
| Gateway | 400 Bad Request | networkId not configured on the gateway, or the contract reverted with one of the above | Check gateway logs and the supported-networks list |
Gateway caveats
The gateway is a serial-submit relay, not a confirmation service. Two consequences:
- Returns on submit, not on inclusion. The 200 response only proves the tx was broadcast. Confirm by polling the block explorer or
eth_getTransactionReceipt. - Single-file queue per service. Drip requests are serialized through one promise chain. A stuck tx blocks the next drip until it resolves or times out.
Operator checklist
After step 1 you should be able to verify, on the same chain you'll use throughout the integration:
IERC20(dst).balanceOf(investor) > 0IDSToken(dst).getDSService(4)returns a non-zero address (the registry service)IDSRegistryService(registry).getInvestor(investor)returns a non-empty string