Zero-knowledge vault service for shielded transfers on EVVM.
ShieldedPool enables privacy-preserving deposits and withdrawals using zkSNARK proofs. Users can deposit tokens into the pool to receive a commitment, then withdraw to any address using zero-knowledge proofs that verify the note exists without revealing the source.
- Deposit: Lock tokens into the pool, receive a shielded commitment
- Transfer: Move funds between commitments privately
- Split: Split one commitment into up to four new commitments
- Withdraw: Withdraw to any address using zk proofs
User -> [Deposit] -> ShieldedPool -> Commitment
|
[Transfer/Split]
|
User <- [Withdraw] <- ShieldedPool
| Contract | Purpose |
|---|---|
ShieldedPool |
Main vault contract |
WithdrawFromPoolVerifier |
Verifier for withdrawal proofs |
SplitNoteVerifier |
Verifier for split proofs |
Deposit tokens into the pool, creating a shielded commitment.
function deposit(
address from, // Token sender
uint256 amount, // Deposit amount
bytes32 commitment, // Shielded commitment (secret)
address senderExecutor, // EVVM executor for sender
address originExecutor, // EVVM executor for origin
uint256 nonce, // Nonce for authorization
bytes signature, // EIP-712 signature from sender
uint256 priorityFeePay, // Priority fee for payment
uint256 noncePay, // Nonce for payment tx
bytes signaturePay // EIP-712 signature for payment
)Requirements:
amount > 0commitmentmust be unique (not previously used)- Both signatures must be valid
Events: Emits Deposit(sender, commitment, amount)
Transfer between shielded commitments.
function transferIntent(
address from,
bytes32 expectedRoot, // Merkle tree root to verify against
bytes proof, // ZK proof
bytes32 nullifierIn, // Nullifier for input note
uint32 merkleProofLength, // Depth of merkle proof (1-10)
bytes32 newCommitment, // New shielded commitment
address senderExecutor,
address originExecutor,
uint256 nonce,
bytes signature
)Requirements:
expectedRootmust be registerednullifierInmust not be spentmerkleProofLengthmust be 1-10
Events: Emits TransferIntent(root, nullifier, newCommitment)
Split one commitment into up to four new commitments.
function split(
address from,
bytes32 expectedRoot,
bytes32 nullifierIn,
uint32 merkleProofLength,
bytes32 newCommitment1,
bytes32 newCommitment2,
bytes32 newCommitment3,
bytes32 newCommitment4,
bytes proof,
address senderExecutor,
address originExecutor,
uint256 nonce,
bytes signature
)Requirements:
- Split verifier must be configured (
address(0)check) - All other requirements same as transferIntent
Events: Emits SplitIntent(root, nullifier, newCommitment1-4)
Withdraw funds to any address using zk proof.
function withdraw(
address from,
bytes proof, // ZK proof
bytes32[] publicInputs, // 6 public inputs
bytes32 ciphertext, // Encrypted amount
address senderExecutor,
address originExecutor,
uint256 nonce,
bytes signature
)Public Inputs (6):
[0] nullifierIn
[1] merkleProofLength
[2] expectedRoot
[3] recipient (cast to bytes32)
[4] padding
[5] decrypted amount
Requirements:
- Withdraw verifier must be configured
publicInputs.length == 6merkleProofLengthmust be 1-10- Root must be registered
- Nullifier must not be spent
- Recipient cannot be zero address
- Amount must be > 0
Events: Emits Withdraw(recipient, nullifier)
Configure which addresses can register new Merkle roots.
function setRootRelayer(address relayer, bool allowed)Access: Owner only
Register a new Merkle root for proof verification.
function registerRoot(bytes32 root)Access: Registered root relayers only
| Error | Description |
|---|---|
"not owner" |
Caller is not contract owner |
"not root relayer" |
Caller is not registered relayer |
"amount must be > 0" |
Deposit/withdraw amount is zero |
"commitment already used" |
Commitment was already deposited |
"root already known" |
Root is already registered |
"unknown root" |
Root is not registered |
"nullifier used" |
Nullifier was already spent |
"invalid merkle depth" |
Merkle proof depth out of range |
"invalid proof" |
ZK proof verification failed |
"invalid recipient" |
Recipient is zero address |
"invalid withdraw amount" |
Withdrawn amount is zero |
"amount mismatch" |
Public amount doesn't match decrypted |
"split not enabled" |
Split verifier not configured |
"withdraw not enabled" |
Withdraw verifier not configured |
- Foundry installed
.envfile configured
# Required
DEPLOYER_PRIVATE_KEY=
EVVM_CORE_ADDRESS=
EVVM_STAKING_ADDRESS=
EVVM_USDC_TOKEN_ADDRESS=
ROOT_RELAYER_ADDRESS=
SEPOLIA_RPC_URL=forge script script/Deploy.s.sol:DeployScript --broadcast --rpc-url sepolia- Add RPC endpoint to
foundry.toml:
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"-
Add to
Deploy.s.solor create network-specific script -
Run:
forge script script/Deploy.s.sol:DeployScript --broadcast --rpc-url mainnet- Verify contracts on Etherscan:
forge verify-contract --compiler-version <version> <contract-address> <contract-name> --ETHERSCAN-API-KEY <key>- Update deployment records (off-repo)
# Install dependencies
npm install
cd noir && npm install
# Build contracts
npm run build
# Build Noir circuits
npm run noir:compile
# Export circuit artifacts
npm run noir:exportnpm run testEVVM Noncommercial License - See LICENSE for details.