3/5
## Setting Up Foundry Tests for a zk-SNARK Mixer Contract This lesson guides you through the initial setup of a testing environment for a zk-SNARK based mixer contract, similar in concept to Tornado Cash, using the robust Foundry testing framework. We'll cover creating the test file, importing necessary contracts, defining state variables, and leveraging Foundry's `setUp` function and Foreign Function Interface (FFI) for more complex input generation. ## Acknowledgements and Essential Resources Before diving into the technical implementation, it's crucial to acknowledge valuable community contributions. A significant resource for understanding the mechanics of Tornado Cash and its testing patterns is Krishang's project, `tornado-cash-rebuilt`, available on GitHub at `github.com/nkrishang/tornado-cash-rebuilt`. This repository served as an excellent learning tool. Notably, Krishang's implementation utilizes original hash functions like Mimic Sponge for the Merkle tree and Pedersen for commitments. While our current implementation will employ Poseidon, the structure and testing approach, particularly within Krishang's `ETHtornado.t.sol` test file, offer valuable insights that have been adapted for this lesson. For further exploration and insightful content on related topics, consider following Krishang on X (formerly Twitter) under the handle `@MonkeyMeaning`. ## Setting Up Your Test File: `Mixer.t.sol` The first step is to create our test file. Within your Foundry project's `test` directory, create a new file named `Mixer.t.sol`. ### Boilerplate and Pragmas Begin with the standard SPDX license identifier and pragma statement for the Solidity compiler version: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; ``` ### Importing Necessary Contracts Next, import the contracts that our test suite will interact with and deploy. This includes the main `Mixer` contract, the `HonkVerifier` for zk-SNARK proof verification, and components for the Merkle tree, specifically `Poseidon2` for hashing. ```solidity import {Mixer} from "../src/Mixer.sol"; import {HonkVerifier} from "../src/Verifier.sol"; // The Verifier contract import {IncrementalMerkleTree, Poseidon2} from "../src/IncrementalMerkleTree.sol"; // For Merkle tree and hashing ``` It's important to use the actual contract name for the verifier. In this case, after checking the `Verifier.sol` file, we use `HonkVerifier`. While `IncrementalMerkleTree` might be an internal component of the `Mixer`, `Poseidon2` is explicitly imported as it will be instantiated and used as the hasher. ### Test Contract Definition Define a new contract that will encapsulate our test functions and state: ```solidity contract MixerTest { // Test logic and state variables will go here } ``` ## Initializing State: Variables and the `setUp` Function To manage our contract instances and test-specific addresses, we'll declare state variables and use Foundry's `setUp` function for initialization. ### State Variables Inside `MixerTest`, declare public state variables for the `Mixer`, `HonkVerifier`, and `Poseidon2` contracts. We'll also define a test `recipient` address using Foundry's `makeAddr` cheatcode for deterministic address generation. ```solidity contract MixerTest { Mixer public mixer; HonkVerifier public verifier; Poseidon2 public hasher; // The Poseidon2 contract instance for hashing address public recipient = makeAddr("recipient"); // Foundry cheatcode to create a named address } ``` ### The `setUp` Function Foundry's `setUp` function is executed before each test case (functions prefixed with `test`). This is where we'll deploy our contracts. ```solidity function setUp() public { // 1. Deploy the Verifier contract verifier = new HonkVerifier(); // 2. Deploy the Hasher contract (Poseidon2) hasher = new Poseidon2(); // 3. Deploy the Mixer contract // The Mixer constructor arguments are: // - IVerifier _verifier: The verifier contract instance. // - IHasher _hasher: The hasher contract instance (e.g., Poseidon2). // - uint32 _merkleTreeHeight: The height of the Merkle tree. mixer = new Mixer(verifier, hasher, 20); // Merkle tree height is set to 20 } ``` In this setup, `HonkVerifier` and `Poseidon2` are deployed without constructor arguments. The `Mixer` contract, however, requires instances of the verifier and hasher, along with the Merkle tree height. A height of `20` is chosen here, aligning with parameters used during proof generation. This implies that `HonkVerifier` conforms to an `IVerifier` interface and `Poseidon2` to an `IHasher` interface, as expected by the `Mixer`'s constructor. ## Structuring Your First Test: `testMakeDeposit` With the setup complete, we can plan our first test function, `testMakeDeposit`. This function will verify the deposit functionality of the `Mixer` contract. ```solidity function testMakeDeposit() public { // Steps for the test: // 1. Create a commitment // 2. Make a deposit into the mixer contract // mixer.deposit(_commitment); // This will need to be payable and send ETH } ``` The core steps involve generating a `commitment` (typically derived from a secret and a nullifier) and then calling the `mixer.deposit()` function with this commitment. It's important to remember that the `deposit` function is `payable` and will require sending ETH with the transaction. ## Generating Test Inputs: Leveraging Foundry's FFI for Commitments Generating zk-SNARK inputs like commitments (e.g., `H(secret, nullifier)`) directly within Solidity can be cumbersome for testing purposes. Foundry's Foreign Function Interface (FFI) provides a powerful solution by allowing tests to call external scripts. We'll outline a helper function, `_getCommitment`, to demonstrate this concept. ```solidity function _getCommitment() internal returns (bytes32) { // This function will use Foundry's FFI to call an external script (e.g., JavaScript). // The script will: // 1. Generate two random 32-byte values (representing, for example, a nullifier and a secret). // 2. Hash these two values together using the Poseidon hash function. // 3. Return the resulting bytes32 commitment. // FFI implementation (conceptual): // string[] memory inputs = new string[](3); // inputs[0] = "node"; // Or python, etc. // inputs[1] = "path/to/your/commitment_generator_script.js"; // inputs[2] = "arguments_for_script_if_any"; // bytes memory result = vm.ffi(inputs); // return abi.decode(result, (bytes32)); // For now, this is a placeholder. Implementation will involve actual FFI calls. // Placeholder return for compilation, replace with actual FFI logic. return bytes32(0); } ``` The FFI mechanism enables our Solidity tests to execute an external script (e.g., written in JavaScript or Python). This script would handle the generation of two random 32-byte values (simulating a nullifier and a secret), hash them using the same Poseidon hash function configured in our smart contracts, and then return the resulting `bytes32` commitment. This approach mirrors the off-chain input preparation that occurs in a real-world application and ensures consistency in hashing algorithms. A similar FFI-based helper will be necessary for generating proofs when testing withdrawal functionality. ## Key Concepts in zk-SNARK Contract Testing This initial setup highlights several crucial concepts for testing zk-SNARK based systems: * **Foundry Framework:** Utilizing powerful features like `setUp` for test initialization, cheatcodes (e.g., `makeAddr`), and FFI for external script interaction. * **Contract Deployment in Tests:** Instantiating and deploying contracts (`new ContractName()`) directly within the testing environment is essential for creating a controlled state. * **Commitment Schemes:** The fundamental cryptographic primitive `commitment = H(secret, nullifier)` is central to the deposit mechanism in privacy-preserving systems. * **Hasher Consistency:** Ensuring the identical hash function (Poseidon2 in this case) is used both in off-chain input generation (via FFI scripts) and within the on-chain smart contracts is paramount for system integrity. * **Foreign Function Interface (FFI):** A vital technique for bridging Solidity tests with external scripts, particularly useful for pre-calculating or generating complex inputs like commitments, witness data, or even proofs for zk-SNARKs. * **Merkle Tree Configuration:** The Merkle tree's height (e.g., `20`) is a critical parameter that must align between the zk-SNARK circuit design and the smart contract implementation. By establishing this testing foundation, we are well-prepared to write comprehensive and effective tests for our zk-SNARK mixer contract, starting with the deposit functionality.
An essential primer on Setting Up Foundry Tests for a zk-SNARK Mixer Contract - Get started by creating your test file, importing key contracts such as the Mixer and Verifier, and initializing state variables. This lesson also covers utilizing Foundry's `setUp` function and its Foreign Function Interface (FFI) for generating zk-SNARK commitments.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025