Skip to main content

Solidity Contracts

Smart contracts for on-chain verification of zero-knowledge proofs generated with ZK-Kit.

Overview

The ZK-Kit Solidity package provides contracts for:

  • Verifying Merkle tree proofs on-chain
  • Verifying zk-SNARK proofs (Groth16, PLONK)
  • Managing Merkle trees in smart contracts
  • Implementing privacy-preserving applications on Ethereum

Repository

Installation

Using npm/yarn

npm install @zk-kit/incremental-merkle-tree.sol

# Or with yarn
yarn add @zk-kit/incremental-merkle-tree.sol

Using Foundry

forge install privacy-scaling-explorations/zk-kit.solidity

Using Hardhat

npm install @zk-kit/incremental-merkle-tree.sol

Add to hardhat.config.js:

module.exports = {
solidity: "0.8.20",
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts"
}
}

Available Contracts

Incremental Merkle Tree

On-chain Incremental Merkle Tree implementation.

Package: @zk-kit/incremental-merkle-tree.sol

Features:

  • Gas-optimized insertions
  • On-chain root verification
  • Event emission for indexing
  • Configurable depth

Usage:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol";

contract MyContract {
using IncrementalBinaryTree for IncrementalBinaryTree.Data;

IncrementalBinaryTree.Data internal tree;

constructor() {
tree.init(20, 0); // depth=20, zeroValue=0
}

function addMember(uint256 commitment) external {
tree.insert(commitment);
}

function getRoot() public view returns (uint256) {
return tree.root;
}
}

Proof Verifiers

Groth16 Verifier

Verifies Groth16 zk-SNARK proofs.

Generated from circuits using:

snarkjs zkey export solidityverifier circuit.zkey verifier.sol

Usage:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./Groth16Verifier.sol";

contract MyZKApp {
Groth16Verifier public verifier;

constructor(address _verifier) {
verifier = Groth16Verifier(_verifier);
}

function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[] memory input
) public view returns (bool) {
return verifier.verifyProof(a, b, c, input);
}
}

PLONK Verifier

Verifies PLONK proofs (no per-circuit trusted setup).

Generated from circuits:

snarkjs zkey export solidityverifier circuit.zkey verifier.sol

Poseidon Hash

On-chain Poseidon hash implementation.

Package: @zk-kit/poseidon.sol

Usage:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/poseidon.sol/Poseidon.sol";

contract MyContract {
function hashTwo(uint256 left, uint256 right) public pure returns (uint256) {
return Poseidon.hash([left, right]);
}

function hashMany(uint256[] memory inputs) public pure returns (uint256) {
return Poseidon.hash(inputs);
}
}

Quick Start Example

Anonymous Voting Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol";
import "./Groth16Verifier.sol";

contract AnonymousVoting {
using IncrementalBinaryTree for IncrementalBinaryTree.Data;

// Merkle tree of registered voters
IncrementalBinaryTree.Data internal voters;

// Proof verifier
Groth16Verifier public verifier;

// Track used nullifiers to prevent double-voting
mapping(uint256 => bool) public nullifiers;

// Vote counts
mapping(uint256 => uint256) public votes;

event VoterRegistered(uint256 commitment);
event VoteCast(uint256 nullifier, uint256 candidate);

constructor(address _verifier) {
voters.init(20, 0);
verifier = Groth16Verifier(_verifier);
}

function register(uint256 commitment) external {
voters.insert(commitment);
emit VoterRegistered(commitment);
}

function vote(
uint256 candidate,
uint256 nullifier,
uint256 root,
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c
) external {
// Verify root is current or recent
require(root == voters.root, "Invalid root");

// Verify nullifier hasn't been used
require(!nullifiers[nullifier], "Already voted");

// Verify proof
uint[] memory publicInputs = new uint[](3);
publicInputs[0] = root;
publicInputs[1] = nullifier;
publicInputs[2] = candidate;

require(
verifier.verifyProof(a, b, c, publicInputs),
"Invalid proof"
);

// Record vote
nullifiers[nullifier] = true;
votes[candidate]++;

emit VoteCast(nullifier, candidate);
}

function getVoterRoot() public view returns (uint256) {
return voters.root;
}
}

Deploy and Use

// deploy.js
const { ethers } = require("hardhat")

async function main() {
// Deploy verifier
const Verifier = await ethers.getContractFactory("Groth16Verifier")
const verifier = await Verifier.deploy()
await verifier.deployed()

// Deploy voting contract
const Voting = await ethers.getContractFactory("AnonymousVoting")
const voting = await Voting.deploy(verifier.address)
await voting.deployed()

console.log("Voting deployed to:", voting.address)
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

API Reference

IncrementalBinaryTree

init

function init(
Data storage self,
uint256 depth,
uint256 zero
) internal

Initializes the tree.

Parameters:

  • self: Tree storage reference
  • depth: Maximum depth
  • zero: Value for empty leaves

Example:

tree.init(20, 0);

insert

function insert(
Data storage self,
uint256 leaf
) internal

Inserts a leaf into the tree.

Parameters:

  • self: Tree storage reference
  • leaf: Value to insert

Example:

tree.insert(commitment);

root

Data storage self.root returns (uint256)

Gets the current root.

Returns: Current Merkle root

Example:

uint256 currentRoot = tree.root;

numberOfLeaves

Data storage self.numberOfLeaves returns (uint256)

Gets the number of leaves in the tree.

Returns: Leaf count

Example:

uint256 count = tree.numberOfLeaves;

Groth16Verifier

verifyProof

function verifyProof(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[] memory input
) public view returns (bool)

Verifies a Groth16 proof.

Parameters:

  • a: Proof point A
  • b: Proof point B
  • c: Proof point C
  • input: Public inputs

Returns: true if proof is valid

Example:

bool isValid = verifier.verifyProof(a, b, c, publicInputs);

Advanced Usage

Batch Verification

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./Groth16Verifier.sol";

contract BatchVerifier {
Groth16Verifier public verifier;

struct Proof {
uint[2] a;
uint[2][2] b;
uint[2] c;
uint[] input;
}

constructor(address _verifier) {
verifier = Groth16Verifier(_verifier);
}

function verifyBatch(Proof[] memory proofs) public view returns (bool) {
for (uint i = 0; i < proofs.length; i++) {
if (!verifier.verifyProof(
proofs[i].a,
proofs[i].b,
proofs[i].c,
proofs[i].input
)) {
return false;
}
}
return true;
}
}

Event-Based Indexing

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol";

contract IndexedTree {
using IncrementalBinaryTree for IncrementalBinaryTree.Data;

IncrementalBinaryTree.Data internal tree;

event LeafInserted(uint256 indexed index, uint256 leaf, uint256 root);

constructor() {
tree.init(20, 0);
}

function insert(uint256 leaf) external {
uint256 index = tree.numberOfLeaves;
tree.insert(leaf);
emit LeafInserted(index, leaf, tree.root);
}
}

Root History

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@zk-kit/incremental-merkle-tree.sol/IncrementalBinaryTree.sol";

contract TreeWithHistory {
using IncrementalBinaryTree for IncrementalBinaryTree.Data;

IncrementalBinaryTree.Data internal tree;

uint256 public constant ROOT_HISTORY_SIZE = 30;
uint256[ROOT_HISTORY_SIZE] public rootHistory;
uint256 public currentRootIndex;

constructor() {
tree.init(20, 0);
}

function insert(uint256 leaf) external {
tree.insert(leaf);

// Store root in history
rootHistory[currentRootIndex] = tree.root;
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
}

function isKnownRoot(uint256 root) public view returns (bool) {
if (root == tree.root) return true;

for (uint256 i = 0; i < ROOT_HISTORY_SIZE; i++) {
if (root == rootHistory[i]) return true;
}

return false;
}
}

Gas Optimization

Tree Insertion Costs

DepthGas per InsertNotes
10~50,000Small tree
16~80,000Medium tree
20~100,000Large tree
32~150,000Very large tree

Optimization Tips

// ✅ Good: Use events for off-chain indexing
emit LeafInserted(index, leaf);

// ✅ Good: Batch operations
function insertBatch(uint256[] memory leaves) external {
for (uint i = 0; i < leaves.length; i++) {
tree.insert(leaves[i]);
}
}

// ✅ Good: Use assembly for critical paths
assembly {
// Gas-optimized operations
}

Testing Contracts

Using Hardhat

const { expect } = require("chai")
const { ethers } = require("hardhat")

describe("AnonymousVoting", function() {
let voting, verifier

beforeEach(async function() {
const Verifier = await ethers.getContractFactory("Groth16Verifier")
verifier = await Verifier.deploy()

const Voting = await ethers.getContractFactory("AnonymousVoting")
voting = await Voting.deploy(verifier.address)
})

it("should register voter", async function() {
await voting.register(123)
expect(await voting.getVoterRoot()).to.not.equal(0)
})

it("should cast vote with valid proof", async function() {
// Generate proof off-chain
const proof = await generateProof(/* ... */)

await voting.vote(
candidate,
nullifier,
root,
proof.a,
proof.b,
proof.c
)

expect(await voting.votes(candidate)).to.equal(1)
})
})

Using Foundry

// test/AnonymousVoting.t.sol
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/AnonymousVoting.sol";

contract AnonymousVotingTest is Test {
AnonymousVoting public voting;

function setUp() public {
Groth16Verifier verifier = new Groth16Verifier();
voting = new AnonymousVoting(address(verifier));
}

function testRegister() public {
voting.register(123);
assertTrue(voting.getVoterRoot() != 0);
}
}
# Run tests
forge test

Best Practices

Security

// ✅ Good: Validate inputs
require(leaf != 0, "Invalid leaf");

// ✅ Good: Check root validity
require(isKnownRoot(root), "Unknown root");

// ✅ Good: Prevent replay attacks
require(!nullifiers[nullifier], "Used nullifier");

// ✅ Good: Use latest Solidity version
pragma solidity ^0.8.20;

Gas Efficiency

// ✅ Good: Use calldata for read-only arrays
function verify(uint[] calldata input) external {
// ...
}

// ✅ Good: Cache storage reads
uint256 currentRoot = tree.root;
if (currentRoot == expectedRoot) {
// Use currentRoot instead of tree.root
}

// ✅ Good: Use unchecked for safe math
unchecked {
i++;
}

Deployment

Mainnet Deployment Checklist

  • Audit smart contracts
  • Test on testnet (Sepolia, Goerli)
  • Verify contracts on Etherscan
  • Set up monitoring and alerts
  • Document contract addresses
  • Implement upgrade mechanism (if needed)

Example Deployment Script

// scripts/deploy-production.js
const { ethers } = require("hardhat")

async function main() {
// Deploy with create2 for deterministic addresses
const factory = await ethers.getContractFactory("AnonymousVoting")

const salt = ethers.utils.id("v1.0.0")
const voting = await factory.deploy(verifierAddress, {
gasLimit: 5000000
})

await voting.deployed()

// Verify on Etherscan
await run("verify:verify", {
address: voting.address,
constructorArguments: [verifierAddress]
})
}

Common Use Cases

  • ✅ Anonymous voting
  • ✅ Private airdrops
  • ✅ Credential verification
  • ✅ Privacy-preserving DeFi
  • ✅ ZK rollups
  • ✅ Identity systems

Source

Community