New Stylus template: ZK based token contracts

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

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.