Skip to main content

Overview

Deposits in Tornado Nova allow users to add funds to the pool and receive shielded UTXOs. A deposit is identified by a positive extAmount value and creates commitments in the Merkle tree.

Deposit Process

The deposit operation consists of these key steps:
1

Create output UTXOs

Generate one or more UTXO outputs that will represent your shielded balance.
2

Prepare transaction

Build the transaction with empty inputs and your output UTXOs.
3

Generate ZK proof

Create a zero-knowledge proof that validates the transaction without revealing details.
4

Submit to pool

Call the transact function to deposit funds and insert commitments.

Deposit Detection

The contract identifies a deposit when extAmount > 0:
contracts/TornadoPool.sol
function transact(Proof memory _args, ExtData memory _extData) public {
  if (_extData.extAmount > 0) {
    // for deposits from L2
    token.transferFrom(msg.sender, address(this), uint256(_extData.extAmount));
    require(uint256(_extData.extAmount) <= maximumDepositAmount, "amount is larger than maximumDepositAmount");
  }

  _transact(_args, _extData);
}
When extAmount > 0, the contract transfers tokens from the sender to the pool before processing the transaction.

ExtAmount Calculation

The external amount is calculated as the difference between outputs and inputs:
src/index.js
let extAmount = BigNumber.from(fee)
  .add(outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
  .sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
For deposits:
  • Inputs: Empty UTXOs (amount = 0)
  • Outputs: New UTXOs with deposited amounts
  • Result: extAmount = outputs.sum() + fee (positive value)

Creating a Deposit

Here’s how to create a deposit transaction:
src/index.js
async function prepareTransaction({
  tornadoPool,
  inputs = [],
  outputs = [],
  fee = 0,
  recipient = 0,
  relayer = 0,
  isL1Withdrawal = false,
  l1Fee = 0,
}) {
  if (inputs.length > 16 || outputs.length > 2) {
    throw new Error('Incorrect inputs/outputs count')
  }
  while (inputs.length !== 2 && inputs.length < 16) {
    inputs.push(new Utxo())
  }
  while (outputs.length < 2) {
    outputs.push(new Utxo())
  }

  let extAmount = BigNumber.from(fee)
    .add(outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
    .sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))

  const { args, extData } = await getProof({
    inputs,
    outputs,
    tree: await buildMerkleTree({ tornadoPool }),
    extAmount,
    fee,
    recipient,
    relayer,
    isL1Withdrawal,
    l1Fee,
  })

  return {
    args,
    extData,
  }
}

UTXO Structure

Each UTXO contains:
src/utxo.js
class Utxo {
  constructor({ amount = 0, keypair = new Keypair(), blinding = randomBN(), index = null } = {}) {
    this.amount = BigNumber.from(amount)
    this.blinding = BigNumber.from(blinding)
    this.keypair = keypair
    this.index = index
  }

  getCommitment() {
    if (!this._commitment) {
      this._commitment = poseidonHash([this.amount, this.keypair.pubkey, this.blinding])
    }
    return this._commitment
  }
}
The commitment is a Poseidon hash of the amount, public key, and blinding factor. This creates a cryptographic commitment that hides the UTXO details.

Deposit Limits

Deposits are subject to maximum amount restrictions:
contracts/TornadoPool.sol
uint256 public constant MIN_EXT_AMOUNT_LIMIT = 0.5 ether;
uint256 public maximumDepositAmount;

require(uint256(_extData.extAmount) <= maximumDepositAmount, "amount is larger than maximumDepositAmount");
Always check the maximumDepositAmount before attempting a deposit. Transactions exceeding this limit will revert.

Commitment Events

After a successful deposit, the contract emits commitment events:
contracts/TornadoPool.sol
emit NewCommitment(_args.outputCommitments[0], nextIndex - 2, _extData.encryptedOutput1);
emit NewCommitment(_args.outputCommitments[1], nextIndex - 1, _extData.encryptedOutput2);
These events contain:
  • commitment: The UTXO commitment hash
  • index: Position in the Merkle tree
  • encryptedOutput: Encrypted UTXO data for the recipient

Cross-Chain Deposits

For deposits from L1, the onTokenBridged function handles the bridging:
contracts/TornadoPool.sol
function onTokenBridged(
  IERC6777 _token,
  uint256 _amount,
  bytes calldata _data
) external override {
  (Proof memory _args, ExtData memory _extData) = abi.decode(_data, (Proof, ExtData));
  require(_token == token, "provided token is not supported");
  require(msg.sender == omniBridge, "only omni bridge");
  require(_amount >= uint256(_extData.extAmount), "amount from bridge is incorrect");
  require(token.balanceOf(address(this)) >= uint256(_extData.extAmount) + lastBalance, "bridge did not send enough tokens");
  require(uint256(_extData.extAmount) <= maximumDepositAmount, "amount is larger than maximumDepositAmount");
  uint256 sentAmount = token.balanceOf(address(this)) - lastBalance;
  try TornadoPool(address(this)).onTransact(_args, _extData) {} catch (bytes memory) {
    token.transfer(multisig, sentAmount);
  }
}
Cross-chain deposits use the OmniBridge. If the transaction fails, funds are sent to the multisig for recovery.

Build docs developers (and LLMs) love