New Stylus template: ZK based token contracts

Yash Kumar

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

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

  1. Connect Wallet: Connect to Arbitrum Sepolia testnet
  2. Generate Proof: Click "Generate ZK Proof" - this proves you have sufficient balance
  3. 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.