10 NFTs for demo purposes on Arbitrum.
On-chain randomness (block timestamp) · Three phases · No reveal needed · IPFS metadata.
Deployed on Arbitrum One:
0x27ccb5c8da8ae0e2c8b349a0a7d14627429c9569
Built using QuantumAILab/ARBuilder
nft_drop/
├── packages/
│ ├── contract/ # Stylus (Rust) ERC-721 — ArbiDrop
│ ├── frontend/ # Next.js 14 · wagmi v2 · RainbowKit · DaisyUI
│ ├── indexer/ # The Graph subgraph — Transfer + Mint events
│ └── oracle/ # Chainlink VRF v2.5 consumer (Solidity, optional)
├── Makefile
└── package.json
| Phase | Window | Price | Max per wallet |
|---|---|---|---|
| 1 — ALLOWLIST | First 15 min | 0.000001 ETH | 2 |
| 2 — PUBLIC | Next 15 min | 0.000001 ETH | 1 |
| 3 — CLOSED | After 30 min | — | — |
TokenIds are drawn at random from a remaining pool using a swap-pop pattern. The block-timestamp entropy is used by default; swap in the VRF oracle for production-grade randomness.
| Tool | Install |
|---|---|
Rust + cargo-stylus |
cargo install cargo-stylus |
Foundry (cast) |
https://getfoundry.sh |
| Node.js ≥ 18 | https://nodejs.org |
| Graph CLI | npm i -g @graphprotocol/graph-cli |
cd packages/contract
# Copy and fill in env
cp ../.env.example .env
# PRIVATE_KEY=0x...
# RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
# https://arb1.arbitrum.io/rpc
# Check + deploy + initialize
bash deploy.sh
# The deployed address is written to deployed_address.txtAfter deployment:
# Add wallets to the allowlist
cast send $CONTRACT "addToAllowlist(address)" 0xYOUR_WALLET \
--private-key $PRIVATE_KEY --rpc-url $RPC_URL
# Set the drop start timestamp (Unix seconds)
cast send $CONTRACT "setStartTimestamp(uint256)" $(date +%s) \
--private-key $PRIVATE_KEY --rpc-url $RPC_URLcd packages/oracle
cp .env.example .env
# Fill in PRIVATE_KEY and VRF_SUBSCRIPTION_ID
npm install
npm run compile
npm run deployThen add the deployed address as a consumer on https://vrf.chain.link.
cd packages/indexer
npm install
# Update subgraph.yaml:
# source.address → your deployed contract address
# source.startBlock → deployment block number
npm run codegen
npm run build
# Authenticate with The Graph Studio
graph auth --studio YOUR_DEPLOY_KEY
npm run deploycd packages/frontend
cp .env.example .env.local
# Fill in:
# NEXT_PUBLIC_CONTRACT_ADDRESS=0x...
# NEXT_PUBLIC_WALLET_CONNECT_ID=... (cloud.walletconnect.com)
# NEXT_PUBLIC_SUBGRAPH_URL=https://api.studio.thegraph.com/query/.../arbidrop/...
npm install
npm run devOpen http://localhost:3000.
make contract-test # Run Rust unit tests
make contract-deploy # Build + deploy contract
make frontend-dev # Start Next.js dev server
make indexer-deploy # Deploy subgraph
make oracle-deploy # Deploy VRF consumer- Phase banner — ALLOWLIST / PUBLIC MINT / SOLD OUT / CLOSED with color coding
- Countdown timer — live client-side tick to next phase boundary
- Mint progress —
7 / 10 mintedwith a progress bar - Wallet eligibility — allowlist status, mints remaining in current phase
- Mint panel — one-click mint with correct ETH value, error/success toasts
- Gallery — last 12 minted NFTs pulled from the subgraph, refreshes every 30s
- Leaderboard — top 5 wallets by mint count, refreshes every 60s
| Function | Description |
|---|---|
initialize() |
One-time setup; fills the 10-token demo pool |
mint() payable |
Mint one NFT; assigns random tokenId |
currentPhase() → uint256 |
0=closed, 1=allowlist, 2=public |
dropStartTime() → uint256 |
Unix ts when allowlist phase begins |
secondsUntilNextPhase() |
Seconds to next phase boundary |
isAllowlisted(addr) |
Check allowlist status |
mintedBy(addr) |
How many tokens minted by address |
totalSupply() |
Total minted so far |
maxSupply() |
10 |
tokenUri(tokenId) |
ipfs://QmPlaceholderCID/{id}.json |
setStartTimestamp(ts) |
Owner only |
addToAllowlist(addr) |
Owner only |
withdraw(to, amount) |
Owner only |
| Event | Signature |
|---|---|
| Transfer | Transfer(indexed address from, indexed address to, indexed uint256 tokenId) |
| Mint | Mint(indexed address minter, indexed uint256 tokenId, uint256 phase) |
Update BASE_URI once metadata is pinned:
# Contract
cast send $CONTRACT "setBaseUri(string)" "ipfs://QmYourRealCID/" \
--private-key $PRIVATE_KEY --rpc-url $RPC_URL
# Frontend — update NEXT_PUBLIC_CONTRACT_ADDRESS and rebuildExpected metadata format ({tokenId}.json):
{
"name": "ArbiDrop #42",
"description": "Early Arbitrum community member NFT",
"image": "ipfs://QmYourImageCID/42.png",
"attributes": [
{ "trait_type": "Phase", "value": "Allowlist" }
]
}- The on-chain random selection uses
block.timestamp % pool_size. This is fine for a low-stakes community drop but is miner-manipulable. For a high-value drop, integrate theArbiDropVRForacle (two-step mint pattern). - The
only_ownerguard is lightweight; consider adding aOwnable2Step-style pattern for mainnet. - Always audit before deploying to mainnet.