ZK Privacy Pools
Zero-knowledge privacy pools are the foundation of Obsqra.fi's privacy model. They enable users to deposit and withdraw funds without creating an on-chain link between the two transactions.
The Problem with Transparent DeFi
Every transaction on Ethereum is publicly visible. When you deposit into a yield protocol, anyone can see your address, your balance, and track your activity. This creates several problems:
Front-running
MEV bots can see your pending transactions and front-run them, extracting value from your trades and deposits.
Copy Trading
Competitors can monitor your treasury's positions and copy your strategy, reducing your alpha.
Targeted Attacks
Large positions become targets for exploits, social engineering, or market manipulation.
Privacy Violations
Your financial activity is permanently recorded and can be analyzed by anyone, forever.
How Privacy Pools Solve This
Privacy pools break the link between deposits and withdrawals using cryptographic commitments and zero-knowledge proofs. Here's the key insight:
"Instead of storing who deposited what, we store cryptographic commitments that can only be redeemed by someone who knows the secret."
When you deposit, you don't reveal your identity. You submit a commitment - a hash of a secret that only you know. When you withdraw, you prove you know the secret without revealing it, using a zero-knowledge proof.
The Commitment System
What is a Commitment?
A commitment is a cryptographic hash that "commits" you to a secret value without revealing it. Think of it like a sealed envelope: you can prove the envelope contains something, but no one can see inside until you open it.
| Component | Purpose | Who Knows It |
|---|---|---|
| Secret | Proves ownership of deposit | Only the depositor |
| Nullifier | Prevents double-spending | Only the depositor (until withdrawal) |
| Commitment | Stored on-chain as deposit record | Public (but reveals nothing) |
Why Poseidon Hash?
We use the Poseidon hash function instead of keccak256 (Ethereum's native hash) because Poseidon is "ZK-friendly" - it requires far fewer constraints in a ZK circuit:
| Hash Function | Constraints in Circuit | Proof Time Impact |
|---|---|---|
| Poseidon | ~240 constraints | Fast (~5 seconds) |
| keccak256 | ~150,000 constraints | Slow (~5 minutes) |
| SHA256 | ~25,000 constraints | Medium (~1 minute) |
The Nullifier System
The nullifier prevents double-spending. When you withdraw, you reveal the nullifier (but not the secret), and the contract marks it as "used." If you try to withdraw again with the same nullifier, the transaction reverts.
Commitment stored
Nullifier revealed and marked
Reverts: nullifier used
Commitment stored
Nullifier revealed and marked
Reverts: nullifier used
Zero-Knowledge Proof Verification
The magic happens in the ZK proof. During withdrawal, you generate a proof that says:
"I know a secret and nullifier such that:
- 1.
Poseidon(secret, nullifier)equals one of the stored commitments - 2.
Poseidon(nullifier)equals the nullifier hash I'm revealing - 3. I want to send the funds to
recipient - 4. The amount is
amount
...without revealing the secret or which commitment is mine."
The Groth16 verifier contract checks this proof in about 200,000 gas. If the proof is valid, the contract knows you're entitled to withdraw, but doesn't know which deposit you're claiming.
What the Verifier Checks
| Check | What It Proves | Public or Private |
|---|---|---|
| Commitment exists | Deposit was made | Public (commitment visible) |
| Nullifier is fresh | Not double-spending | Public (nullifier revealed) |
| Proof is valid | Prover knows the secret | Private (secret hidden) |
| Amounts match | Correct withdrawal amount | Public (in transaction) |
Privacy Guarantees
Let's be precise about what privacy you get and what you don't:
✓ What's Hidden
- • Which deposit you're withdrawing
- • Your depositor address
- • Link between deposit and withdrawal
- • Your total position in the pool
⚠ What's Visible
- • That a deposit happened (amount visible)
- • That a withdrawal happened (amount visible)
- • Withdrawal recipient address
- • Total pool TVL
Implementation Details
Deposit Function
function deposit(
bytes32 commitment,
uint256 amount
) external returns (uint256 index) {
require(amount > 0, "InvalidAmount");
require(!commitments[commitment], "CommitmentAlreadyUsed");
// Store commitment
commitments[commitment] = true;
depositIndex++;
// Transfer wETH from user
IERC20(weth).transferFrom(msg.sender, address(this), amount);
// Deploy to strategies
_allocateToStrategies(amount);
emit Deposit(commitment, amount, depositIndex);
return depositIndex;
}Withdraw Function
function withdraw(
bytes32 nullifier,
address recipient,
uint256 amount,
bytes calldata proof
) external {
require(!nullifierUsed[nullifier], "NullifierAlreadyUsed");
// Verify ZK proof
require(
verifier.verifyProof(proof, [commitment, nullifier, recipient, amount]),
"InvalidProof"
);
// Mark nullifier as used
nullifierUsed[nullifier] = true;
// Transfer funds
IERC20(weth).transfer(recipient, amount);
emit Withdraw(nullifier, recipient, amount);
}