Documentation
Solidity 0.8.28 Launch App →
BSC • Chainlink VRF v2.5 • Foundry

Fun Offering

A casino-styled token presale contract for BNB Smart Chain. Buyers pick a tier, pay in BNB, and receive a random amount of tokens within that tier's range — powered by Chainlink VRF for provably fair on-chain randomness. Payments are forwarded instantly to the contract owner.

Overview

This project contains two parts:

What the contract does:

ℹ Note
The frontend currently implements the BNB purchase flow only. The stablecoin purchase function exists in the contract and can be called directly, but there is no UI for it yet.

Prerequisites

Installation

Purchase & download

Purchase the source code on web3.market. You'll receive a .zip file.

Extract the archive

unzip fun-offering.zip -d fun-offering
cd fun-offering

Install Foundry dependencies

forge install

This pulls OpenZeppelin, Chainlink, and forge-std into the lib/ directory.

Set up environment variables

cp .env.example .env

Edit .env with your values:

# RPC endpoints
BSC_RPC_URL=https://bsc-dataseed1.binance.org
BSC_TESTNET_RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545

# Your BSCScan API key (for contract verification)
BSCSCAN_API_KEY=your_api_key_here

# Deployer wallet private key (with BNB for gas)
DEPLOYER_PRIVATE_KEY=your_private_key_here

Build & test the contracts

forge build
forge test -vvv

All tests should pass. The test suite includes unit tests, fuzz tests, and integration tests.

Install frontend dependencies

cd frontend
npm install

Project Structure

src/ FunOffering.sol ← main presale contract DummyToken.sol ← test ERC-20 token (optional) script/ DeployAll.s.sol ← deployment scripts test/ FunOffering.t.sol ← test suite (25 tests) mocks/ ← mock contracts for testing frontend/ src/ App.tsx ← main widget UI main.tsx ← React entry + providers index.css ← all styles (Tailwind + custom) config/ contract.ts ← ABI + contract address wagmi.ts ← Reown AppKit + wallet config hooks/ useFunOffering.ts ← contract interactions useTransactionHistory.ts ← purchase history components/ SlotMachine.tsx ← spinning number animation lib/ sounds.ts ← synthesized sound effects .env ← frontend environment vars package.json foundry.toml ← Foundry config .env ← contract environment vars

Contract Overview

FunOffering.sol is the main contract. It inherits from:

ParentSourcePurpose
VRFConsumerBaseV2PlusChainlinkReceives VRF random numbers. Also provides ownership via ConfirmedOwner.
ReentrancyGuardOpenZeppelin v5Protects against reentrancy on buy functions.
PausableOpenZeppelin v5Emergency pause capability.
ℹ Ownership
The contract owner is set to the deployer address. Ownership comes from Chainlink's ConfirmedOwner (part of VRFConsumerBaseV2Plus), not from OpenZeppelin's Ownable.

Constructor parameters:

ParameterTypeDescription
_offeringTokenaddressThe ERC-20 token to distribute to buyers
_vrfCoordinatoraddressChainlink VRF v2.5 coordinator address
_priceFeedaddressChainlink BNB/USD price feed address
_vrfSubscriptionIduint256Your Chainlink VRF subscription ID
_vrfKeyHashbytes32VRF gas lane key hash
_vrfCallbackGasLimituint32Gas limit for the VRF callback (recommended: 300000)
_vrfRequestConfirmationsuint16Block confirmations before VRF response (recommended: 3)
_maxTotalDistributionuint256Maximum total tokens to distribute (18 decimals)

Deploy Your Token

You can use any existing ERC-20 token, or deploy the included DummyToken for testing. The DummyToken mints 10,000,000 tokens to the deployer and has an owner-only mint() function.

# Deploy DummyToken to BSC Testnet
forge script script/DeployAll.s.sol:DeployDummyToken \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --broadcast

Save the deployed token address — you'll need it for the next step.

✓ Tip
For production, you'll use your own token contract. The FunOffering contract works with any standard ERC-20 token (18 decimals expected).

Before deploying the main contract, you need a Chainlink VRF v2.5 subscription:

Create a VRF subscription

Go to vrf.chain.link and create a new subscription on the BSC network (testnet or mainnet).

Fund the subscription

Add LINK tokens to your subscription. The contract uses LINK (not native BNB) to pay for VRF requests. Each request costs a small amount of LINK.

Note your subscription ID

You'll find the subscription ID on the VRF dashboard. This is a large number — you'll need it for the deployment script.

⚠ Important
After deploying the FunOffering contract, you must add the contract address as a consumer on your VRF subscription. The contract cannot receive random numbers until this is done.

Deploy FunOffering

The deployment script (script/DeployAll.s.sol) is pre-configured for BSC Testnet. For mainnet, you'll need to update the hardcoded addresses in the script (see Network Addresses).

Set additional environment variables

# Add to your .env file
OFFERING_TOKEN=0xYourTokenAddress
VRF_SUBSCRIPTION_ID=your_subscription_id_number

Review the deployment parameters

Open script/DeployAll.s.sol and verify these values in the DeployFunOffering contract:

// Callback gas limit: 300,000 is sufficient for most cases
300_000,
// Request confirmations: 3 blocks
3,
// Max distribution cap (18 decimals)
1_000_000e18  // = 1,000,000 tokens

Change the maxTotalDistribution value to match the total tokens you want to distribute during your presale.

Deploy

# Load env vars and deploy to BSC Testnet
source .env

forge script script/DeployAll.s.sol:DeployFunOffering \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --broadcast

The console will print the deployed contract address.

Verify on BSCScan (optional)

forge verify-contract <CONTRACT_ADDRESS> src/FunOffering.sol:FunOffering \
  --chain-id 97 \
  --constructor-args $(cast abi-encode "constructor(address,address,address,uint256,bytes32,uint32,uint16,uint256)" \
    $OFFERING_TOKEN \
    0xDA3b641D438362C440Ac5458c57e00a712b66700 \
    0x2514895c72f50D8bd4B4F9b1110F0D6bD2c97526 \
    $VRF_SUBSCRIPTION_ID \
    0x8596b430971ac45bdf6088665b9ad8e8630c9d5049ab54b14dff711bee7c0e26 \
    300000 \
    3 \
    1000000000000000000000000) \
  --etherscan-api-key $BSCSCAN_API_KEY

Deploying to BSC Mainnet

To deploy to mainnet, edit the addresses in script/DeployAll.s.sol:

// Change from BSC Testnet to BSC Mainnet
address vrfCoordinator = 0xd691f04bc0C9a24Edb78af9E005Cf85768F694C9;
address priceFeed     = 0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE;

// Use the mainnet key hash from Chainlink docs
bytes32 keyHash = 0x...; // see docs.chain.link for BSC mainnet key hashes

Then deploy using $BSC_RPC_URL instead of the testnet URL.

Post-Deployment Setup

After deploying the contract, you need to complete these steps from the owner wallet:

Add the contract as a VRF consumer

Go to vrf.chain.link, open your subscription, and click "Add consumer". Paste the deployed FunOffering contract address.

Add purchase tiers

Call addTier() for each tier you want. See Configure Tiers for details.

Fund the contract with tokens

Transfer your offering tokens directly to the contract address using a standard ERC-20 transfer(). The contract distributes tokens from its own balance.

# Example using cast (Foundry CLI)
cast send $OFFERING_TOKEN \
  "transfer(address,uint256)" \
  <FUNOFFERING_ADDRESS> \
  1000000000000000000000000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

The value above is 1,000,000 tokens (with 18 decimals). Make sure the contract holds at least maxTotalDistribution tokens.

⚠ Critical
If the contract runs out of tokens, VRF callbacks will revert and buyers will lose their payment without receiving tokens. Always ensure the contract balance covers the maximum distribution cap.

Configure Tiers

Tiers define the purchase ranges. Each tier has:

FieldDecimalsDescription
minBuyUsd8Minimum purchase in USD
maxBuyUsd8Maximum purchase in USD
minTokens18Minimum tokens the buyer can receive
maxTokens18Maximum tokens the buyer can receive
⚠ Decimal format
USD values use 8 decimals to match the Chainlink price feed. So $10 = 10_00000000 (1,000,000,000). Token values use 18 decimals, so 1,000 tokens = 1000_000000000000000000 (1000e18).

Adding tiers with cast

# Add a tier: $10-$50 buy-in, 1,000-5,000 tokens
cast send <CONTRACT> \
  "addTier(uint256,uint256,uint256,uint256)" \
  1000000000 5000000000 1000000000000000000000 5000000000000000000000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

# Add a tier: $50-$200 buy-in, 5,000-20,000 tokens
cast send <CONTRACT> \
  "addTier(uint256,uint256,uint256,uint256)" \
  5000000000 20000000000 5000000000000000000000 20000000000000000000000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

# Add a tier: $200-$1000 buy-in, 20,000-100,000 tokens
cast send <CONTRACT> \
  "addTier(uint256,uint256,uint256,uint256)" \
  20000000000 100000000000 20000000000000000000000 100000000000000000000000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

Updating an existing tier

# Update tier 0: new prices and token range, keep active
cast send <CONTRACT> \
  "updateTier(uint256,uint256,uint256,uint256,uint256,bool)" \
  0 \
  2000000000 10000000000 \
  2000000000000000000000 10000000000000000000000 \
  true \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

Disabling a tier

# Deactivate tier 2
cast send <CONTRACT> \
  "setTierActive(uint256,bool)" 2 false \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY
ℹ Tier display
The frontend automatically reads tier data from the contract. It shows all active tiers in a grid. Up to 4 tiers are supported by the UI (the display names and colors are defined in App.tsx — see Customize UI).

Stablecoin Payments

The contract supports purchases with whitelisted stablecoins via buyWithToken(). Stablecoin amounts are normalized to 8-decimal USD values for comparison against tier bounds.

# Whitelist USDT on BSC Mainnet
cast send <CONTRACT> \
  "setWhitelistedStablecoin(address,bool)" \
  0x55d398326f99059fF775485246999027B3197955 true \
  --rpc-url $BSC_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

# Whitelist USDC on BSC Mainnet
cast send <CONTRACT> \
  "setWhitelistedStablecoin(address,bool)" \
  0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d true \
  --rpc-url $BSC_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY
ℹ Note
The frontend does not include a UI for stablecoin purchases. The buyWithToken() function can be called directly via BSCScan or a script. Stablecoin payments require the buyer to first approve() the contract to spend their tokens.

Distribution Cap

The maxTotalDistribution value limits how many tokens can be distributed in total. Once reached, no more purchases are possible.

# Check current distribution
cast call <CONTRACT> "totalDistributed()" --rpc-url $BSC_TESTNET_RPC_URL
cast call <CONTRACT> "maxTotalDistribution()" --rpc-url $BSC_TESTNET_RPC_URL

# Increase the cap to 2,000,000 tokens
cast send <CONTRACT> \
  "setMaxTotalDistribution(uint256)" \
  2000000000000000000000000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

The frontend displays a progress bar showing totalDistributed / maxTotalDistribution.

VRF Parameters

These can be adjusted after deployment if needed:

# Increase callback gas limit (if VRF callbacks are failing)
cast send <CONTRACT> \
  "setVrfCallbackGasLimit(uint32)" 400000 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY

# Change request confirmations
cast send <CONTRACT> \
  "setVrfRequestConfirmations(uint16)" 5 \
  --rpc-url $BSC_TESTNET_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY
ParameterDefaultNotes
vrfCallbackGasLimit300,000Increase if callbacks run out of gas. Max depends on the coordinator.
vrfRequestConfirmations3More confirmations = slower but more secure. Minimum is usually 3.

Frontend Setup & Run

Configure environment variables

cd frontend
cp .env.example .env

Edit frontend/.env:

# Your deployed FunOffering contract address
VITE_CONTRACT_ADDRESS=0xYourContractAddress

# Reown (WalletConnect) project ID from cloud.reown.com
VITE_WALLETCONNECT_PROJECT_ID=your_project_id

Install and run

npm install
npm run dev

The app will be available at http://localhost:5173.

Build for production

npm run build

Output goes to frontend/dist/.

Available scripts

CommandDescription
npm run devStart development server with hot reload
npm run buildType-check and build for production
npm run previewPreview the production build locally
npm run testRun tests once
npm run test:watchRun tests in watch mode
npm run lintRun ESLint

Deploy to Vercel

Push to GitHub

Push your repository to GitHub (or GitLab/Bitbucket).

Import in Vercel

Go to vercel.com/new and import your repository.

Set the Root Directory to frontend in the project settings.

Add environment variables

In the Vercel project settings, add:

KeyValue
VITE_CONTRACT_ADDRESSYour deployed contract address
VITE_WALLETCONNECT_PROJECT_IDYour Reown project ID

Deploy

Vercel will auto-detect Vite and build. Your app will be live at your-project.vercel.app.

This documentation page will be available at your-project.vercel.app/docs/.

✓ Tip
Vercel's default build settings work out of the box for this project. Framework preset: Vite. Build command: npm run build. Output directory: dist.

Customize UI

Tier names and colors

The tier display names and colors are defined in frontend/src/App.tsx:

const TIER_META = [
  { name: 'BRNZ', color: '#cd7f32' },  // Tier 0 - Bronze
  { name: 'SLVR', color: '#8892a4' },  // Tier 1 - Silver
  { name: 'GOLD', color: '#ffb800' },  // Tier 2 - Gold
  { name: 'DMND', color: '#00ff41' },  // Tier 3 - Diamond
]

Edit the name and color values to match your branding. Names are displayed as short labels (4 characters recommended). If you have fewer tiers, the unused entries are simply ignored. Add more entries if you have more than 4 tiers.

Title and metadata

The app title appears in several places:

Colors and styling

All styles are in frontend/src/index.css. The main accent color #00ff41 (neon green) appears throughout. To change it, do a find-and-replace in that file. Key color values:

ColorUsage
#00ff41Primary accent (buttons, highlights, status indicators)
#00e5ffSecondary accent (slot machine glow)
#ffb800Amber (loading states, jackpot effects)
#050505 / #0a0a0aBackground colors

The wallet modal accent is set separately in config/wagmi.ts:

createAppKit({
  // ...
  themeMode: 'dark',
  themeVariables: {
    '--apkt-accent': '#00ff41',
  },
})

Fonts

The app uses two Google Fonts loaded in index.html:

To change fonts, update the Google Fonts <link> in index.html and the font-family rules in index.css.

Sound effects

All sounds are synthesized via the Web Audio API in lib/sounds.ts — no audio files are needed. You can adjust frequencies, durations, and waveforms there. Users can mute sounds with the toggle button in the widget header.

Iframe Embedding

The widget can be embedded in any website via iframe. Two URL parameters are supported:

ParameterDescription
?embed=trueRemoves the background and outer padding for seamless embedding
?contract=0x...Overrides the contract address (useful for multiple deployments)
<!-- Embed example -->
<iframe
  src="https://your-app.vercel.app/?embed=true"
  width="500"
  height="800"
  style="border: none; border-radius: 12px;"
></iframe>

<!-- With contract override -->
<iframe
  src="https://your-app.vercel.app/?embed=true&contract=0xABC..."
  width="500"
  height="800"
  style="border: none;"
></iframe>

Owner Functions

All administrative functions are restricted to the contract owner (the deployer address). These can be called using cast send or via BSCScan's "Write Contract" interface.

FunctionDescription
addTier()Add a new purchase tier
updateTier()Update all parameters of an existing tier
setTierActive()Enable or disable a tier
setWhitelistedStablecoin()Whitelist or remove a stablecoin
setMaxTotalDistribution()Change the distribution cap
setVrfCallbackGasLimit()Adjust VRF callback gas
setVrfRequestConfirmations()Adjust VRF block confirmations
withdrawBNB()Withdraw any BNB held by the contract
withdrawTokens()Withdraw any ERC-20 tokens from the contract
pause()Pause all purchases
unpause()Resume purchases

Pause & Unpause

The contract can be paused to prevent new purchases. Pending VRF callbacks will still be processed.

# Pause the contract
cast send <CONTRACT> "pause()" \
  --rpc-url $BSC_TESTNET_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY

# Unpause the contract
cast send <CONTRACT> "unpause()" \
  --rpc-url $BSC_TESTNET_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY

Withdraw Funds

Normally, BNB payments are forwarded to the owner address instantly during each purchase. These withdrawal functions are for edge cases where funds end up in the contract.

# Withdraw any BNB balance to a specific address
cast send <CONTRACT> "withdrawBNB(address)" <RECIPIENT> \
  --rpc-url $BSC_TESTNET_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY

# Withdraw any ERC-20 token balance
cast send <CONTRACT> "withdrawTokens(address,address)" <TOKEN> <RECIPIENT> \
  --rpc-url $BSC_TESTNET_RPC_URL --private-key $DEPLOYER_PRIVATE_KEY
⚠ Warning
Withdrawing the offering token from the contract will reduce the available supply for distribution. Only do this if you intend to end the presale or reduce the cap.

Architecture

Smart contract

Frontend

Purchase Flow

Here's what happens when a user buys tokens:

User selects a tier and enters a BNB amount

The frontend calculates the USD equivalent using the on-chain price feed and validates it falls within the tier's USD range.

Transaction is sent

The user's wallet prompts to confirm a buyWithBNB(tierId) transaction with the BNB amount as msg.value.

Contract processes payment

The contract converts the BNB to USD via the Chainlink price feed, validates the amount, forwards the BNB to the owner, and requests a random number from Chainlink VRF.

VRF callback

After a few blocks, Chainlink delivers a random number. The contract calculates: minTokens + (randomWord % (maxTokens - minTokens + 1)) and transfers that many tokens to the buyer.

Frontend reveals the result

The frontend watches for the PurchaseRevealed event. When it arrives, the slot machine animation decelerates and locks in the final amount, followed by confetti.

Frontend state machine

idle → buying → spinning → revealing → revealed
 ↑                                                    |
 ←—————————— reset —————————————←

idle      → User is browsing tiers and entering amounts
buying    → Transaction submitted, waiting for wallet confirmation
spinning  → Transaction confirmed, waiting for Chainlink VRF response
revealing → VRF arrived, slot machine ramp-down animation playing
revealed  → Final amount displayed, confetti, share button visible

Contract API

Write functions

FunctionParamsDescription
buyWithBNB(uint256) _tierId + BNB as msg.value Purchase tokens with BNB. Payable.
buyWithToken(uint256,address,uint256) _tierId, _token, _amount Purchase with a whitelisted stablecoin. Requires prior approval.

Read functions

FunctionReturnsDescription
getTierCount()uint256Number of tiers
getTier(uint256)TierTier struct (minBuyUsd, maxBuyUsd, minTokens, maxTokens, isActive)
totalDistributed()uint256Total tokens distributed so far (18 decimals)
maxTotalDistribution()uint256Distribution cap (18 decimals)
getLatestBnbUsdPrice()int256Current BNB/USD price (8 decimals)
offeringToken()addressThe ERC-20 token being distributed
owner()addressContract owner address

Events

EventIndexedFields
PurchaseRequested requestId, buyer tierId, paymentAmount, paymentToken
PurchaseRevealed requestId, buyer tierId, tokenAmount
TierAddedtierIdminBuyUsd, maxBuyUsd, minTokens, maxTokens
TierUpdatedtierId
StablecoinWhitelistedtokenstatus

Network Addresses

BSC Testnet (Chain ID: 97)

ResourceAddress
VRF Coordinator0xDA3b641D438362C440Ac5458c57e00a712b66700
BNB/USD Price Feed0x2514895c72f50D8bd4B4F9b1110F0D6bD2c97526
VRF Key Hash0x8596b430971ac45bdf6088665b9ad8e8630c9d5049ab54b14dff711bee7c0e26

BSC Mainnet (Chain ID: 56)

ResourceAddress
VRF Coordinator0xd691f04bc0C9a24Edb78af9E005Cf85768F694C9
BNB/USD Price Feed0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE
VRF Key HashSee Chainlink docs
USDT0x55d398326f99059fF775485246999027B3197955
USDC0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
ℹ Chainlink docs
Always verify addresses against the official Chainlink documentation before deploying to mainnet: VRF Supported Networks and Price Feed Addresses.

Running Tests

Smart contract tests

# Run all tests with verbose output
forge test -vvv

# Run a specific test
forge test --match-test test_buyWithBNB_success -vvv

# Run with gas reporting
forge test --gas-report

The test suite covers:

Frontend tests

cd frontend
npm test

The frontend has basic rendering tests using Vitest and React Testing Library.


Built with Foundry, React, and Chainlink VRF