New Stylus template: ZK based token contracts

We have added support for creating and deploying ZK Proof based token contracts with Stylus. The templates provide you with Stylus contracts, ZK circuit files, and a ready-to-deploy Next.js app + API for proof generation and minting.
This guide walks you through building a privacy-preserving ERC721 token system using Zero-Knowledge proofs on Arbitrum Stylus. Users can mint tokens by proving they own a minimum amount of ETH without revealing their exact balance.
What You'll Build
- ZK Circuit: Proves token ownership without revealing exact balances
- Stylus Contract: Rust-based ERC721 contract that verifies ZK proofs
- Frontend: Next.js app for generating proofs and minting tokens
- Oracle System: Secure balance verification mechanism
Prerequisites
- Node.js (>= 20.18.0)
- pnpm package manager
- Rust with cargo
- circom for ZK circuits
- Stylus CLI for contract deployment
Step 1: Project Setup
Using thirdweb CLI
npx thirdweb create-stylus
Select "Stylus ZK ERC721" from the dropdown menu. This will:
- Clone the repository to your machine
- Set up the project structure
- Install basic dependencies
Manual Setup (Alternative)
git clone https://github.com/thirdweb-example/stylus-zk-erc721.git
cd stylus-zk-erc721
Step 2: Install Dependencies
Install dependencies for all components:
# Install root dependencies
pnpm install
# Install circuit dependencies
cd circuits
pnpm install
cd ..
# Install frontend dependencies
cd app
pnpm install
cd ..
Step 3: Generate Cryptographic Keys
Run the setup script to generate oracle keys and build the ZK circuit:
chmod +x setup.sh
./setup.sh
This script will:
- Generate a random oracle secret key
- Inject the secret into the ZK circuit
- Compile the circuit with circom
- Generate proving and verification keys
- Create the trusted setup for Groth16
⚠️ Important: The oracle secret is critical for security. Keep it private!
Step 4: Deploy the Contract
Using thirdweb CLI
cd contracts
npx thirdweb@latest deploy-stylus -k <THIRDWEB_SECRET_KEY>
Using Stylus CLI (Alternative)
cd contracts
cargo stylus deploy --endpoint arbitrum-sepolia
Copy the deployed contract address - you'll need it for the frontend.
Step 5: Configure the Frontend
Update the contract address in your frontend:
cd app
# Edit pages/index.tsx or lib/config.ts
# Update ZK_MINT_CONTRACT_ADDRESS with your deployed address
Create environment file:
# app/.env.local
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
ORACLE_SECRET_KEY=<your_oracle_secret_from_setup>
Step 6: Run the Application
cd app
pnpm dev
# Visit http://localhost:3000
Step 7: Test the System
- Connect Wallet: Connect to Arbitrum Sepolia testnet
- Generate Proof: Click "Generate ZK Proof" - this proves you have sufficient balance
- Mint Tokens: Use the proof to mint ERC721 tokens
Customizing the ZK Logic
Understanding the Circuit
The core circuit (circuits/token_ownership.circom
) has these components:
template TokenOwnership(oracle_secret) {
// Private inputs (hidden from public)
signal input actual_balance; // Real balance from oracle
signal input salt; // Randomness for uniqueness
// Public inputs (visible on-chain)
signal input min_required_balance; // Minimum threshold
signal input token_contract_hash; // Which token to check
signal input user_address_hash; // User's address hash
signal input timestamp; // When oracle signed data
signal input oracle_commitment; // Oracle's commitment
// Output
signal output nullifier; // Prevents replay attacks
}
Customization Examples
1. Change Balance Threshold Logic
Replace the balance check with custom logic:
// Original: Simple greater-than check
component gte = GreaterEqThan(64);
gte.in[0] <== actual_balance;
gte.in[1] <== min_required_balance;
gte.out === 1;
// Custom: Logarithmic scaling
component log_check = LogarithmicCheck();
log_check.balance <== actual_balance;
log_check.threshold <== min_required_balance;
log_check.out === 1;
2. Add Multiple Token Support
Extend the circuit to verify multiple token balances:
template MultiTokenOwnership(oracle_secret, num_tokens) {
signal input actual_balances[num_tokens];
signal input min_required_balances[num_tokens];
signal input token_addresses[num_tokens];
// Verify each token separately
component checks[num_tokens];
for (var i = 0; i < num_tokens; i++) {
checks[i] = GreaterEqThan(64);
checks[i].in[0] <== actual_balances[i];
checks[i].in[1] <== min_required_balances[i];
checks[i].out === 1;
}
}
3. Add Time-Based Constraints
Add expiration logic to proofs:
// Add time validation
component time_check = LessThan(64);
time_check.in[0] <== timestamp;
time_check.in[1] <== max_timestamp; // New public input
time_check.out === 1;
Rebuilding After Changes
After modifying the circuit:
cd circuits
pnpm run build
# Re-inject verification keys
cd ..
node scripts/inject-vk.js
# Recompile contract
cd contracts
cargo build
Additional Resources
Support
Need help? Please reach out to our support team.