Skip to main content

PoolVault contract

PoolVault is the core contract. It holds user funds (native ETH and ERC20), maintains the state tree of timestamped commitment and nullifier 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 contract this pool reads from.

Main functions

deposit(...) Called via 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).
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