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:
Create output UTXOs
Generate one or more UTXO outputs that will represent your shielded balance.
Prepare transaction
Build the transaction with empty inputs and your output UTXOs.
Generate ZK proof
Create a zero-knowledge proof that validates the transaction without revealing details.
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:
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:
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:
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.