Tornado cash

Tornado cash is a fixed-pool commitment and deposit verification workflow

Tornado cash is a fixed-pool deposit workflow in which the user selects a chain, asset and exact denomination before generating a private note and sending its commitment on-chain. A successful transaction moves the chosen amount into one pool contract, appends the commitment to that contract's Merkle tree and emits a Deposit event that the user can verify from the receipt.

On this page

The key decisions happen before confirmation. The wallet must match the selected network, the balance must cover the denomination plus gas and the saved note must derive the same commitment that appears in the transaction.

Pool selection before the wallet prompt

Pool selection prevents the most common deposit mistake: treating the amount field as flexible when each Tornado Cash Classic contract accepts one exact denomination. Choose the network, asset and listed amount in that order, then compare all three with the wallet request.

On Ethereum, the ETH series comprises 0.1 ETH, 1 ETH, 10 ETH and 100 ETH. DAI instances accept 100 DAI, 1 000 DAI, 10 000 DAI and 100 000 DAI, while cDAI uses 5 000 cDAI, 50 000 cDAI, 500 000 cDAI and 5 000 000 cDAI. USDC and USDT each expose 100-unit and 1 000-unit pools; WBTC uses 0.1 WBTC, 1 WBTC and 10 WBTC. Each denomination points to a separate contract and Merkle tree. Selecting 1 ETH therefore never creates a claim on the 10 ETH instance. These figures are contract parameters, not live quotes.

A screen that offers Ethereum, Arbitrum, Optimism or Gnosis Chain also targets different deployments. Token symbols alone don't resolve the contract because the same native symbol can appear on more than one EVM network. Match the wallet network and ERC-20 asset first, then confirm the exact amount and instance address before the interface generates any secret material. That sequence fixes the destination contract at the decision point.

Gas, approvals and the amount that leaves the wallet

Deposit cost has two parts: the fixed pool denomination and network gas, with an extra approval transaction for an ERC-20 asset. An ETH deposit uses 1 contract transaction; a token deposit uses 2 when a new approval must precede the deposit call. The fee that the network charges never changes the denomination credited to the commitment.

Worked example, with every changing input marked hypothetical: suppose an Ethereum wallet selects the 1 ETH pool, the interface estimates 1 100 000 gas and the gas price is 20 gwei. Multiplying 1 100 000 by 20 gwei gives 22 000 000 gwei, or 0.022 ETH. The wallet therefore needs 1.022 ETH for that confirmed case: 1 ETH enters the pool and 0.022 ETH covers the deposit transaction. An ERC-20 route adds the gas for an approval only when the existing allowance sits below the selected denomination. Gas remains outside the fixed pool amount.

How do you confirm the private note before depositing?

Confirm the private note by deriving its commitment locally, then comparing that commitment with the value that the client prepares for the selected pool contract.

Each note preimage combines a 31-byte nullifier with a 31-byte secret, producing exactly 62 bytes before hashing. The Classic client applies a Pedersen hash and encodes the commitment as a 32-byte value compatible with the Baby Jubjub construction. MiMC then labels the Merkle tree nodes. The note's hexadecimal payload contains 124 characters after its 0x prefix, while the human-readable prefix carries the currency, denomination and chain ID. That prefix helps the client locate the right deployment; the secret payload recreates the commitment.

For Ethereum, chain ID 1 appears in that prefix. Tornado CLI parses those fields before it selects a contract, so a local commitment mismatch should stop the confirmation before the wallet signs.

Overlapping gold and silver cryptocurrency coins

The commitment written by the deposit call

The deposit call sends one 32-byte commitment to the selected instance, which inserts it as the next leaf in its Merkle tree.

For ETH, the call carries the instance denomination as transaction value. An ERC-20 instance invokes the token transfer path against the approved balance. OpenZeppelin's reentrancy guard wraps the state-changing call, while the instance rejects a commitment that its commitment mapping already records. It increments the next leaf index and calculates a new MiMC root after insertion, keeping that pool's state separate from every other denomination.

Classic tooling reconstructs a 20-level tree from ordered Deposit events. That depth accommodates 1 048 576 leaf positions because each level doubles the previous capacity.

Execution then sets the commitment's stored flag to true and emits the event. No zero-knowledge proof runs during deposit; Groth16 proof generation belongs to the later withdrawal step. The asset transfer and one new tree leaf form the immediate state change, and the selected instance records both.

Reading the receipt and Deposit event

Receipt verification establishes that the chosen instance accepted the transaction and emitted a Deposit event for the commitment derived from the saved note.

Start with the 32-byte transaction hash that the wallet returns, then request its receipt through Ethereum JSON-RPC or a block explorer. A receipt status of 1 records successful execution; status 0 records a reverted call and leaves no deposit state. Next, compare the receipt's destination with the chosen pool contract and inspect its logs. The Deposit event contains three declared values: the indexed commitment, a 32-bit leaf index and a timestamp. Recompute the note's commitment and require an exact match. A matching event, contract address and successful receipt verify the on-chain deposit without disclosing the note. The topic is examined in practice.

Recovering from a wallet-network mismatch

A wallet-network mismatch occurs when MetaMask or WalletConnect signs for a chain that differs from the pool selected in the interface.

Compare the wallet's chain ID before requesting confirmation: Ethereum uses 1, Optimism uses 10, Gnosis Chain uses 100 and Arbitrum One uses 42161. A Classic note also carries a chain ID in its prefix. If the wallet rejects the request before broadcasting, switch to the intended chain, reconnect the account and reopen the same pending selection. Recheck the asset and denomination because changing networks also changes the target contract.

If the wallet already returned a transaction hash, query the receipt on the chain that actually received it before trying again. A missing hash means no transaction reached the network. A present successful receipt anchors the commitment to that chain's pool and guides the interface back to the correct deployment.

Advanced checks before a later withdrawal

Advanced deposit checks tie the saved note to one commitment, one pool instance and one unspent nullifier before any later proof generation.

Tornado CLI rebuilds the 20-level tree, finds the commitment's leaf and asks whether the contract recognizes the resulting root. It also checks that the nullifier hash remains unspent. At withdrawal, the verifier receives six public inputs: Merkle root, nullifier hash, recipient, relayer, fee and refund. The contract requires a known root, rejects a reused nullifier and caps the fee at the pool denomination. Those later conditions explain why deposit verification must preserve the exact note, chain and instance together. Once the Deposit event matches the locally derived commitment, the interface has the complete on-chain evidence required for that note's lifecycle.

Does WalletConnect change the selected Tornado Cash pool contract?

No, WalletConnect only carries the connection between the interface and the chosen wallet; it doesn't replace the pool contract. The chain, asset and denomination still resolve to one deployed instance. Before signing, compare the wallet's chain ID, destination contract and transaction value with the interface selection. MetaMask and other EVM wallets follow the same boundary: the connector handles communication, while the signed transaction defines the on-chain target.

What happens after an ERC-20 approval confirms but the deposit transaction doesn't?

The approval remains a separate on-chain state change, while the deposit commitment hasn't entered the pool. Reopen the deposit view, confirm the same network, token and denomination and read the existing allowance before submitting the deposit call again. Don't assume the approval receipt proves a deposit. Only a successful receipt directed to the pool instance, together with its matching Deposit event, records the commitment in that pool's Merkle tree.

Could two deposits into the same denomination produce the same note?

Two properly generated deposits use independent random nullifiers and secrets, so their notes and commitments differ even inside the same denomination. Each preimage contains 62 bytes, split into two 31-byte values, before the client derives a 32-byte commitment. The contract also tracks submitted commitments and rejects an exact duplicate. Equal deposit amounts place commitments in the same pool; they don't make the private note or leaf value identical.

Does selecting a pool reserve a Merkle tree position before confirmation?

Selecting a pool doesn't reserve a Merkle tree position; the contract assigns the next leaf index only during successful execution. Generating or saving a note remains an off-chain action, and a wallet signature alone doesn't change the tree. If the transaction stays unsubmitted or reverts, the commitment remains absent. Once execution succeeds, the contract increments its index, stores the commitment and emits the Deposit event with the assigned 32-bit leaf index.

Do token decimals change the fixed amount shown for a pool?

Token decimals change the integer encoding, not the human-readable denomination assigned to the pool. On Ethereum, DAI uses 18 decimals, USDC and USDT use 6 decimals and WBTC uses 8 decimals. An interface converts 100 USDC into its smallest-unit integer before calling the contract. The displayed pool still represents 100 USDC, and both the allowance and token balance must cover that exact encoded amount.

Could a deposit note be verified without connecting the depositing wallet?

Yes, note verification is a read-only calculation and doesn't require the original depositing wallet to connect. A client derives the 32-byte commitment from the note, selects the chain and instance encoded by the note and searches that contract's Deposit events. It then compares the commitment and reads the nullifier's spent status. The wallet matters for signing a new transaction, while public JSON-RPC data supports this verification path.

What causes an interface to miss a confirmed Deposit event?

Stale event caches, the wrong RPC network or the wrong pool instance can make an interface miss a confirmed Deposit event. Start from the successful receipt and identify its chain, destination contract and block number. Then reload events for that exact instance through another JSON-RPC provider or a locally built Tornado Cash Classic client. The note-derived commitment should match one log; changing the note or submitting another deposit doesn't repair an indexing gap.