# PoolVault contract

> PoolVault is the core contract. It holds user funds (native ETH and ERC20), maintains the [state tree](/concepts/state-tree) of timestamped [commitment and nullifier](/concepts/commitments-and-nullifiers) leaves, and tracks spent nullifiers.

PoolVault is the core contract. It holds user funds (native ETH and ERC20), maintains the [state tree](/concepts/state-tree) of timestamped [commitment and nullifier](/concepts/commitments-and-nullifiers) leaves, and tracks spent nullifiers.

## State

`commitments(uint256) → uint256` Maps a commitment to the `block.timestamp` at insertion. A non-zero value means the commitment exists.
`spentNullifiers(uint256) → uint256` Maps a nullifier to the `block.timestamp` at spend. A non-zero value means the note is spent.
`roots(uint256 index) → uint256` / `currentRootIndex() → uint32` The state-tree roots live in a circular root buffer (inherited from `RootBufferUpgradeable`) holding the last `rootHistorySize()` roots, 16 on the deployed configuration. There is no `stateRoot()` getter. Read the latest root as `roots(currentRootIndex())`, and use `isKnownRoot(uint256)` to test membership in the recent-root history.
`aspRegistry() → address` Pointer to the [ASPRegistry](/protocol/contracts/asp-registry) contract this pool reads from.

## Main functions

`deposit(...)` Called via [`Entrypoint`](/protocol/contracts/entrypoint). Receives the deposited funds, inserts a commitment, and emits a `Deposited` event plus a `Note` event (the `Note` event carries only the `hint` and encrypted `data`, not the commitment).
`transact(proof, transactParams, noteData[])` Spends N inputs and produces M outputs; the Groth16 proof struct is the first argument. Permissionless, but `transactParams.processor` must equal `msg.sender`, and `keccak256(transactParams, noteData)` must match the proof's `context`. Validates the state root against the buffer, the keystore root via `Keystore.isKnownRoot`, and the ASP root by equality with `latestASPRoot()`; dispatches to the `transact_NxM` verifier for `(N, M)`; marks the input nullifiers spent and inserts the new commitments; then pays `amountOut` of `tokenIdOut` to `msg.sender`. A pure in-pool transfer must carry `tokenIdOut == address(0)` (`PoolVault_TokenIdMustBeZeroOnInternalTransfer`). Pausable.
`ragequit(...)` Emergency public exit for any unspent note. Gated by `require(msg.sender == _proof.ownerAddress())`. The caller must be the keystore-registered owner of the note, which in v2 is not necessarily the original depositor (notes are transferable via `transact`). It checks the keystore root but never the ASP root, and it is the one entry point with **no** `whenNotPaused` guard, so it works while the pool is paused. That is the non-custodial guarantee expressed in code.

## Errors you'll see

`PoolVault_InvalidTransactProof()` The proof doesn't verify against the on-chain vkey, which usually means the local zkey and the deployed verifier come from different deployment sets.
`PoolVault_NullifierAlreadySpent()` (selector `0x66f527f1`) An input note is already spent on-chain, which indicates local state drift (see [verify spent](../../operations/verify-spent)).
`PoolVault_InvalidProcessor()` Access-control check: the `transactParams.processor` calldata field does not match `msg.sender`. The address that submits `transact` must be the processor the proof was built for.
`PoolVault_ProofContextMismatch()` Integrity check: the proof's `context` does not equal `keccak256(transactParams, noteData)` reduced into the field, so any tampering with the transact params or note data after proving trips this.
`PoolVault_InvalidASPRoot()` The proof's `associationSetRoot` does not equal the registry's current `latestASPRoot()`. `transact` requires the supplied ASP root to be the latest one (strict equality), not merely some historical root. Rebuild the proof against the current root.
`PoolVault_InvalidDepositProof()` The deposit proof doesn't match the deposit verifier's vkey. This is the same deployment-set mismatch as transact.

## Events

`Note(bytes32 indexed hint, bytes data)` Emitted once per *published* `noteData` entry, independent of the output-commitment count (not once per output note). `hint` is an indexed topic: a random 32-byte value for anonymous and discoverable notes, or the recipient-chosen `tag` for payment-request notes, which lets the recipient filter `eth_getLogs` efficiently. For discoverable notes `data` is the opaque encrypted payload (ephemeral X25519 pubkey ‖ ciphertext). A deposit made without discovery data instead carries an empty-data entry (`data = "0x"`), which `PoolVault.deposit` emits unconditionally. In a `transact`, out-of-band recipient outputs publish no `noteData` entry at all, so no Note event is emitted for them. Their commitment is still inserted into the state tree, and only the sender's change note is announced.
`Transacted(...)` A per-transact action record, used by ASP indexers.
`Ragequit(...)` Public ragequit event. Emits `ragequitter` (the commitment owner who called the function), `asset`, `value`, `commitment`, `nullifierHash`, and `label`. The original depositor is not recorded on-chain, though the ASP can link a ragequit back to its deposit via the public `label`.

Source: `v2-monorepo/packages/contracts/src/contracts/PoolVault.sol`
