@lazysuperheroes/fungible-faucet
v2.2.0
Published
Hedera fungible token faucet suite -- simple open faucet, NFT batch faucet (TIER), and NFT gamified faucet (ZERO)
Maintainers
Readme
@lazysuperheroes/fungible-faucet
A consolidated Hedera fungible token faucet suite providing three distribution models: a simple open faucet, a per-serial NFT batch faucet (TIER model), and a gamified NFT faucet with wallet-level cooldowns and FT balance scaling (ZERO model).
Built on Hedera Token Service (HTS) and Solidity 0.8.12.
Table of Contents
- Overview
- Faucet Modes
- Token Lifecycle
- Prerequisites
- Installation
- Configuration
- Usage
- Contract APIs
- Project Structure
- Testing
- npm Scripts Reference
- Related Packages
- License
Overview
This package provides three Solidity smart contracts for distributing fungible tokens on Hedera, along with deployment scripts, operational tooling, and pre-built configuration templates.
Both contracts emit FaucetMessage events for all significant operations, making them fully auditable via the Hedera mirror node.
Faucet Modes
FungibleFaucet (Simple)
An open faucet where any address can pull a fixed daily amount of tokens, subject to a 24-hour cooldown per address.
- Uses HTS
transferFrom(allowance-based) -- the treasury grants an allowance to the faucet contract, which transfers tokens on behalf of the treasury. - Two pull methods:
pullFaucetHTS()(native HTS) andpullFaucetETH()(ERC-20 compatible). - Owner can update the daily amount, treasury address, and token address at any time.
Real-world example: The $LAZY testnet faucet distributes 20 $LAZY per day to any wallet that asks. No NFT required, no cost -- just a cooldown timer.
NFTFaucet (TIER Model -- Per-Serial Batch)
A per-serial batch faucet where each NFT serial accrues claim time independently. Users pass an array of owned serials (up to 256) to claim accumulated tokens in a single transaction. Optional LAZY payment/burn per claim.
- Per-serial accrual: Each serial tracks its own last-claim timestamp. Unclaimed time accumulates up to
maxTimeUnitsperiods. - Batch claiming:
pullFaucetHTS(uint256[])processes multiple serials at once -- efficient for holders with many NFTs. - Boost serials: Designated serials earn
boostPercentageextra on top of the basedailyAmt. - Start time baseline:
_startTimeresets each time the faucet is unpaused, preventing retroactive claims. - Optional LAZY burn: Set
lazyPmtAmt > 0to charge $LAZY per claim (burned via SCT). Set to 0 for free claims. - Treasury-based distribution: Tokens are transferred from an external treasury via HTS allowance (
this.transferFrom).
Real-world example: The TIER faucet rewards WokeFemmes NFT holders with $TIER tokens daily. No claim cost -- just hold the NFT and claim. Each serial accrues independently.
ZeroFaucet (ZERO Model -- Gamified)
A gamified NFT faucet with wallet-level cooldowns, single boost serial selection, FT balance scaling, and precision math for high-decimal tokens. LAZY payment per claim.
- Wallet-level cooldown: One claim per wallet per
minTimeperiod (not per-serial). - Single boost serial: Pass one boost serial per claim for an extra percentage multiplier.
- FT balance scaling: Your balance of a separate "claim FT" multiplies the payout.
- NFT count scaling: Payout scales with how many NFTs you hold (capped at
maxNFTPerClaim). - Precision math: Uses configurable precision scalars for tokens with many decimal places.
- Contract-held distribution: Tokens are held by the contract and transferred via
transferToken.
Real-world example: The XZERO faucet distributes $ZER0X tokens to ZER0_PIXEL NFT holders. Each claim costs 10 $LAZY (burned). Payout depends on ZER0_PIXEL count, $ZER0 balance, and whether you use a boosted serial.
Key features (NFTFaucet and ZeroFaucet)
- Owner can pause/unpause the faucet and manage boost serials at any time
disableUpdates()permanently locks all configuration (irreversible) -- boost management and serial resets remain available- Deploys in a paused state by default for safety
- All operations emit
FaucetMessageevents, fully auditable via the Hedera mirror node
Token Lifecycle
- Create a fungible token on Hedera (for production use, see
@lazysuperheroes/hedera-ft-sc). - Treasury holds tokens -- a treasury account or Smart Contract Treasury holds the token supply.
- Grant allowance to the faucet contract -- the treasury approves the faucet to spend tokens on its behalf.
- Deploy the faucet -- deploy
FungibleFaucetorNFTFaucetwith the appropriate constructor arguments. - Faucet distributes tokens -- users call
pullFaucetHTS()to claim their allocation.
For the NFT faucet, additional setup is required: an NFT collection for gating, a $LAZY token for claim payments, and a Smart Contract Treasury that implements IBurnableHTS.
Prerequisites
- Node.js (v16+)
- Hedera account with sufficient HBAR for contract deployment and gas
- A fungible token created on Hedera
- A treasury holding the fungible token with an allowance granted to the faucet contract address
For the NFT faucet, you also need:
- An NFT collection on Hedera (the claim gate)
- The $LAZY token and a Smart Contract Treasury implementing
IBurnableHTS - A claim FT whose balance factors into claim calculation
Installation
git clone https://github.com/Burstall/hedera-SC-LAZY-Faucet.git
cd hedera-SC-LAZY-Faucet
npm installConfiguration
Quick Start
Copy one of the pre-built templates from configs/ to .env in the project root:
| Template | Mode | Model | Description |
|---|---|---|---|
| configs/lazy-testnet.env.example | Simple | -- | $LAZY faucet on testnet. Pre-filled, just add credentials. |
| configs/tier-mainnet.env.example | NFT-gated | Free | TIER faucet for WokeFemmes holders. No claim cost. |
| configs/xzero-mainnet.env.example | NFT-gated | Paid | XZERO faucet for ZER0_PIXEL holders. 10 $LAZY per claim, burned. |
| configs/custom.env.example | Either | -- | Annotated template for forking with your own token. |
Or copy the combined template:
cp .env.example .envEnvironment Variables
Required for all modes:
| Variable | Description |
|---|---|
| ACCOUNT_ID | Your Hedera account ID (e.g. 0.0.12345) |
| PRIVATE_KEY | ED25519 or ECDSA private key |
| ENVIRONMENT | TEST for testnet, MAIN for mainnet |
| CONTRACT_NAME | FungibleFaucet or NFTFaucet |
| CONTRACT_ID | Set after deployment |
| EVENT_NAME | Event name for log decoding (default: FaucetMessage) |
Simple faucet (FungibleFaucet):
| Variable | Description | Default |
|---|---|---|
| TOKEN_ADDRESS | Fungible token to distribute (EVM address or Hedera ID) | |
| TREASURY_ADDRESS | Treasury that holds tokens and grants allowance | |
| DAILY_AMOUNT | Tokens distributed per pull | 20 |
NFT-gated faucet (NFTFaucet):
| Variable | Description | Default |
|---|---|---|
| TOKEN_ID | Fungible token to distribute | |
| LAZY_TOKEN_ID | $LAZY token address (claim payment) | |
| LAZY_SCT_CONTRACT_ID | LAZY Smart Contract Treasury (for burning) | |
| LAZY_CLAIM_AMOUNT | $LAZY cost per claim (set to 0 for free claims -- skips transfer and burn) | 10 |
| NFT_TOKEN_ID | NFT collection that gates access | |
| CLAIM_TOKEN | FT whose balance multiplies the claim | |
| DAILY_AMT | Base daily accrual rate | 1000 |
| BOOST_PERCENTAGE | Extra percentage for holders of boosted serials | 50 |
| MIN_TIME | Minimum seconds between claims | 86400 |
| MAX_NFTS | Maximum NFTs counted toward claim calculation | 100 |
| DECIMALS | Decimal places of the distributed token | 8 |
Usage
Compile Contracts
npm run compileDeploy
Simple faucet:
npm run simple:deployAfter deployment, copy the printed contract ID into CONTRACT_ID in your .env file.
NFT-gated faucet:
npm run nft:deployPull Tokens
Simple faucet:
npm run simple:pull -- -hts # Pull via HTS
npm run simple:pull -- -eth # Pull via ERC-20NFT-gated faucet:
npm run nft:pull # Pull with NFT serial
npm run nft:check # Check claimable amount firstManage the Faucet
Simple faucet operations:
npm run simple:update-daily # Update daily distribution amount
npm run simple:withdraw -- -lazy -wallet 0.0.XXXXX -amount 100 # Withdraw tokens
npm run simple:withdraw -- -hbar -wallet 0.0.XXXXX -amount 50 # Withdraw HBAR
npm run simple:drain # Drain all HBAR and tokens to operatorNFT faucet operations:
npm run nft:boost-add # Add boost serials
npm run nft:boost-remove # Remove boost serials
npm run nft:pause # Pause the faucet
npm run nft:unpause # Unpause the faucet
npm run nft:lock # Permanently lock settings (irreversible)
npm run nft:stats # Check claim statistics
npm run nft:monitor # Monitor claims via ethers event listenerCommon Operations
npm run info # Query contract storage, balance, auto-renew info
npm run logs # Fetch and decode FaucetMessage events from mirror node
npm run decode-error # Decode smart contract error messages
npm run extract-abi # Extract ABIs from compiled artifacts into abi/Contract APIs
FungibleFaucet
Constructor:
constructor(address lsct, address lazy, uint256 dailyAmt)| Parameter | Description |
|---|---|
| lsct | Address of the treasury (Lazy Smart Contract Treasury) |
| lazy | Address of the fungible token to distribute |
| dailyAmt | Number of tokens per pull (in smallest unit) |
Public / External Functions:
| Function | Access | Description |
|---|---|---|
| pullFaucetHTS() | Public | Pull tokens via HTS transferFrom |
| pullFaucetETH() | Public | Pull tokens via ERC-20 transferFrom |
| getLastPull() | View | Returns caller's last pull timestamp |
| getDailyAmount() | View | Returns current daily distribution amount |
| updateDailyAmount(uint256) | Owner | Set new daily distribution amount |
| getLSCT() | View | Returns current treasury address |
| updateLSCT(address) | Owner | Set new treasury address |
| getLazyToken() | View | Returns current token address |
| updateLazyToken(address) | Owner | Set new token address |
| transferHbar(address payable, uint256) | Owner | Recover HBAR sent to the contract |
Event:
event FaucetMessage(string msgType, address indexed from, address indexed to, uint256 amount)NFTFaucet (TIER -- Per-Serial Batch)
Constructor:
constructor(
address fungibleSCT, // Treasury holding the distributed tokens
address fungible, // Fungible token to distribute (immutable)
address claimNFT, // NFT collection gating access
uint256 dailyAmt, // Tokens per claim period per serial
uint256 boostPercentage,// Boost % for boosted serials (e.g. 50 = +50%)
uint256 minTime, // Claim period in seconds
uint8 maxTimeUnits, // Max accrual periods (e.g. 5 = 5x daily if minTime is 24h)
address lazySCT, // LAZY SC Treasury for burning (address(0) to disable)
address lazyFT, // LAZY token address (address(0) to disable)
uint8 lazyPmtAmt // LAZY charged per claim (0 to disable)
)Public / External Functions:
| Function | Access | Description |
|---|---|---|
| pullFaucetHTS(uint256[]) | Public | Batch claim for up to 256 serials at once |
| getClaimableAmount(uint256[]) | Public | Preview claimable amount for owned serials |
| getClaimableForTokens(uint256[]) | Public | Preview claimable amount (ignores ownership) |
| checkAllowance() | View | Verify caller has sufficient $LAZY allowance |
| addBoostSerials(uint[]) | Owner | Add serials to the boost list (max 255 per call) |
| removeBoostSerials(uint[]) | Owner | Remove serials from the boost list |
| getBoostSerials() | View | Returns all boosted serial numbers |
| updatePauseStatus(bool) | Owner | Pause or unpause (unpause resets _startTime) |
| getPaused() | View | Returns current pause state |
| disableUpdates() | Owner | Permanently lock all settings (irreversible) |
| getDailyAmount() / updateDailyAmount(uint256) | View / Owner | Tokens per period per serial |
| getMinTime() / updateMinTime(uint256) | View / Owner | Claim period in seconds |
| getMaxTimeUnits() / updateMaxTimeUnits(uint8) | View / Owner | Max accrual periods |
| getBoostMultipler() / updateBoostMultiplier(uint256) | View / Owner | Boost percentage |
| getSCT() / updateSCT(address) | View / Owner | Token treasury address |
| getClaimToken() / updateClaimToken(address) | View / Owner | Claim NFT address |
| getLazyPmtAmt() / updateLazyPmtAmt(uint8) | View / Owner | $LAZY cost per claim |
| getLazySCT() / updateLazySCT(address) | View / Owner | LAZY SC Treasury |
| getLazyFT() / updateLazyFT(address) | View / Owner | $LAZY token address |
| getFungibleToken() | View | Distributed fungible token (immutable) |
| resetSerialTimestamp(uint[], uint256) | Owner | Reset serial timestamps for admin adjustment |
| transferHbar(address payable, uint256) | Owner | Recover HBAR sent to the contract |
Event:
event FaucetMessage(string msgType, address indexed from, address indexed to, uint256 amount, uint256 timestamp)ZeroFaucet (ZERO -- Gamified)
Constructor:
constructor(
address lazySCT, // Smart Contract Treasury (implements IBurnableHTS)
address lazyFT, // $LAZY token for claim payment
uint8 lazyPmtAmt, // $LAZY cost per claim
address fungible, // Fungible token to distribute
address claimNFT, // NFT collection gating access
address claimFT, // FT balance used in claim calculation
uint256 dailyRate, // Base daily accrual rate
uint256 boostPercentage,// Boost multiplier for special serials (e.g. 50 = 50%)
uint256 minTime, // Minimum seconds between claims
uint8 maxNFTPerClaim // Max NFTs counted per claim
)Public / External Functions:
| Function | Access | Description |
|---|---|---|
| pullFaucetHTS(uint256 serial) | Public | Claim tokens (pass boost serial or 0) |
| getClaimableAmount(uint256 serial) | Public | Preview claimable amount for a serial |
| checkAllowance() | View | Verify caller has sufficient $LAZY allowance |
| addBoostSerials(uint[]) | Owner | Add serials to the boost list (max 255 per call) |
| removeBoostSerials(uint[]) | Owner | Remove serials from the boost list |
| getBoostSerials() | View | Returns all boosted serial numbers |
| updatePauseStatus(bool) | Owner | Pause or unpause the faucet |
| getPaused() | View | Returns current pause state |
| disableUpdates() | Owner | Permanently lock all settings (irreversible) |
| getDailyAmount() / updateDailyAmount(uint256) | View / Owner | Daily accrual rate |
| getMinTime() / updateMinTime(uint256) | View / Owner | Minimum claim interval |
| getBoostMultipler() / updateBoostMultiplier(uint256) | View / Owner | Boost percentage |
| getMaxNFTPerClaim() / updateMaxNFTPerClaim(uint8) | View / Owner | Max NFTs per claim |
| getLazyPmtAmt() / updateLazyPmtAmt(uint8) | View / Owner | $LAZY cost per claim |
| getLazySCT() / updateLazySCT(address) | View / Owner | Smart Contract Treasury |
| getLazyFT() / updateLazyFT(address) | View / Owner | $LAZY token address |
| getClaimToken() / updateClaimToken(address) | View / Owner | Claim NFT address |
| getClaimFT() / updateClaimFT(address) | View / Owner | Claim FT address |
| getFungibleToken() | View | Distributed fungible token (immutable) |
| resetSerialTimestamp(uint[], uint256) | Owner | Reset serial timestamps for admin adjustment |
| transferHbar(address payable, uint256) | Owner | Recover HBAR sent to the contract |
Event:
event FaucetMessage(
string msgType,
address indexed from,
address indexed to,
uint256 amount,
uint256 apy,
uint256 timestamp
)IBurnableHTS
interface IBurnableHTS {
function burn(address token, uint32 amount) external returns (int256 responseCode);
}Any Smart Contract Treasury that the NFTFaucet or ZeroFaucet uses for $LAZY burning must implement this interface.
Project Structure
contracts/
FungibleFaucet.sol # Simple open faucet
NFTFaucet.sol # TIER per-serial batch faucet
ZeroFaucet.sol # ZERO gamified faucet
IBurnableHTS.sol # Interface for SCT burn
HederaTokenService.sol # Shared Hedera HTS precompile wrappers
IHederaTokenService.sol # HTS interface definitions
HederaResponseCodes.sol # Hedera response code constants
ExpiryHelper.sol # Token expiry helper
FeeHelper.sol # Custom fee helper
KeyHelper.sol # Token key helper
LAZYTokenCreator.sol # SCT implementation (used in tests)
AddrArrayLib.sol # Address array library for LAZYTokenCreator
scripts/
simple/ # FungibleFaucet operational scripts
deploy.js
pullFaucet.js
updateDailyAmt.js
withdrawFunds.js
withdrawToWallet.js
nft/ # NFTFaucet operational scripts
deploy.js
pullFaucet.js
checkClaimableAmount.js
checkClaimStats.js
addBoostSerials.js
removeBoostSerials.js
pause.js
unPause.js
lockFaucet.js
monitorClaimEthers.js
withdrawFunds.js
withdrawToWallet.js
common/ # Shared operational scripts
extractABI.js
getContractInfo.js
getContractLogs.js
loadBytecodeHederaFS.js
decodeSmartContractError.js
utils/ # Shared utilities
args.js
contractHelpers.js
hederaClient.js
test/
FungibleFaucet.test.js # Simple faucet test suite
NFTFaucet.test.js # TIER batch faucet test suite
ZeroFaucet.test.js # ZERO gamified faucet test suite
configs/ # Pre-built .env templates
lazy-testnet.env.example # Simple faucet, testnet, pre-filled
tier-mainnet.env.example # TIER batch faucet, mainnet
xzero-mainnet.env.example # ZERO gamified faucet, mainnet
custom.env.example # Annotated fork template
abi/ # Extracted ABIs (generated by extract-abi)Testing
Tests run against Hedera consensus (testnet or a local Hedera node). They require a .env file with valid test account credentials and sufficient HBAR for contract deployment and gas.
# Run all test suites
npm test
# Run a specific suite
npx hardhat test test/FungibleFaucet.test.js
npx hardhat test test/NFTFaucet.test.js
npx hardhat test test/ZeroFaucet.test.jsThe Hardhat config sets a long Mocha timeout (10,000,000ms) to accommodate Hedera consensus latency.
npm Scripts Reference
Build and Lint
| Script | Command | Description |
|---|---|---|
| compile | npx hardhat compile | Compile all Solidity contracts |
| extract-abi | node scripts/common/extractABI.js | Extract ABIs into abi/ |
| test | npx hardhat test | Run all test suites |
| solhint | solhint -f table contracts/FungibleFaucet.sol contracts/NFTFaucet.sol contracts/ZeroFaucet.sol | Lint Solidity |
| lint | npx eslint scripts/ | Lint JavaScript |
Simple Faucet (FungibleFaucet)
| Script | Description |
|---|---|
| simple:deploy | Deploy the FungibleFaucet contract |
| simple:pull | Pull tokens (pass -hts or -eth flag) |
| simple:update-daily | Update daily distribution amount |
| simple:withdraw | Withdraw tokens or HBAR to a specific wallet |
| simple:drain | Drain all HBAR and tokens back to operator |
NFT Faucet (NFTFaucet)
| Script | Description |
|---|---|
| nft:deploy | Deploy the NFTFaucet contract |
| nft:pull | Pull tokens with NFT serial |
| nft:check | Check claimable amount for a serial |
| nft:boost-add | Add serials to the boost list |
| nft:boost-remove | Remove serials from the boost list |
| nft:pause | Pause the faucet |
| nft:unpause | Unpause the faucet |
| nft:lock | Permanently lock faucet settings |
| nft:stats | Check claim statistics |
| nft:monitor | Monitor claims with ethers event listener |
Common
| Script | Description |
|---|---|
| info | Query contract storage, balance, and auto-renew info |
| logs | Fetch and decode FaucetMessage events from mirror node |
| decode-error | Decode smart contract error messages via 4byte.directory |
Forking Guide
Want to run your own faucet for your token? Here's how.
Step 1: Choose your model
| Question | FungibleFaucet | NFTFaucet (TIER) | ZeroFaucet (ZERO) |
|---|---|---|---|
| Who can claim? | Anyone | NFT holders only | NFT holders only |
| What do they pay? | Nothing | Optional $LAZY per claim | $LAZY per claim (burned) |
| Claim amount logic? | Fixed daily amount | Per-serial accrual with boost | Scales with NFT count + FT balance + boost |
| Batch claiming? | N/A | Yes (up to 256 serials) | No (single boost serial) |
| Need an NFT collection? | No | Yes | Yes |
| Need a burn mechanism? | No | Optional | Yes (SCT implementing IBurnableHTS) |
| Can lock settings? | No | Yes (disableUpdates()) | Yes (disableUpdates()) |
Step 2: Create your token
You need a fungible token on Hedera. For production, use @lazysuperheroes/hedera-ft-sc which creates a token with a Smart Contract Treasury that manages supply and implements IBurnableHTS.
For testing, any HTS fungible token works -- the test suites create throwaway tokens automatically.
Step 3: Configure and deploy
# 1. Clone and install
git clone https://github.com/Burstall/hedera-SC-LAZY-Faucet.git
cd hedera-SC-LAZY-Faucet
npm install
# 2. Copy the template that matches your model
cp configs/custom.env.example .env
# Edit .env with your token IDs, account, and settings
# 3. Compile
npm run compile
# 4. Deploy
npm run simple:deploy # for FungibleFaucet
# or
npm run nft:deploy # for NFTFaucetStep 4: Fund the faucet
The two contracts use different distribution mechanisms:
FungibleFaucet -- allowance-based (treasury keeps tokens)
The treasury (an EOA or Smart Contract Treasury) holds the tokens and grants an allowance to the faucet contract. The faucet calls transferFrom on each pull, drawing from the treasury on demand.
Treasury holds tokens
--> grants allowance to faucet contract
--> faucet calls transferFrom(treasury, user, amount)Setup: use AccountAllowanceApproveTransaction from the Hedera SDK, or your SCT's approveAllowance method.
To refill: increase the allowance. To cut off: revoke the allowance. The treasury retains custody of tokens at all times.
NFTFaucet -- contract holds tokens directly
The faucet contract itself holds the distribution tokens. You send tokens directly to the contract address after deployment. The faucet calls transferToken from its own balance on each claim.
Send tokens to faucet contract address
--> faucet calls transferToken(contract, user, amount)Setup: transfer tokens to the contract's account ID using a standard TransferTransaction.
To refill: send more tokens to the contract. The contract holds full custody.
| | Allowance (FungibleFaucet) | Contract holds (NFTFaucet) | |---|---|---| | Token custody | Treasury retains tokens | Faucet contract holds tokens | | Refill | Increase allowance | Send more tokens to contract | | Drain risk | Limited by allowance amount | Entire contract balance exposed | | Setup complexity | Grant allowance (one SDK call) | Transfer tokens (one SDK call) | | Works with EOA treasury? | Yes | N/A (no treasury involved) | | Works with SCT treasury? | Yes | N/A (no treasury involved) |
Step 5: Unpause (NFT faucet only)
The NFT faucet deploys paused. Once you've verified the configuration:
npm run nft:unpauseCustomization points
- Daily amount:
DAILY_AMOUNT(simple) orDAILY_AMT(NFT) -- how much is distributed per period - Cooldown period: Fixed at 24 hours for the simple faucet; configurable via
MIN_TIMEfor the NFT faucet - Boost multiplier:
BOOST_PERCENTAGE-- extra reward percentage for holders of designated NFT serials - Max NFTs:
MAX_NFTS-- cap on how many NFTs count toward a claim (prevents whales from dominating) - Claim cost:
LAZY_CLAIM_AMOUNT-- set to 0 for free claims, or any amount for a paid/deflationary model
Code Style
- Solidity: Compiler
^0.8.12, optimizer enabled (200 runs). Linted with solhint. - JavaScript: Tabs for indentation, single quotes, semicolons required, trailing commas in multiline. See
.eslintrc.json.
Related Packages
@lazysuperheroes/hedera-ft-sc-- Token creation and Smart Contract Treasury management
