Atomic privacy-preserving token swap combining OCash (ZK privacy pool) with PancakeSwap V3 (DEX).
Users swap tokens without exposing their on-chain identity. A single atomic transaction executes: OCash withdraw → PancakeSwap swap → OCash deposit. The user's EOA never appears on-chain; a relayer submits the transaction and is compensated via OCash's built-in relayerFee mechanism.
User (off-chain) Relayer BSC
│ │ │
│ 1. getQuote() │ │
│ ──[QuoterV2 eth_call]──────────────────────────> │
│ <─── quote ────────────────────────────────────── │
│ │ │
│ 2. prepare() │ │
│ [select UTXOs] │ │
│ [generate ZK proof with │ │
│ recipient=PrivacySwap] │ │
│ │ │
│ 3. submit() or │ │
│ buildTransaction() │ │
│ ──POST /privacy-swap──────> │ │
│ │ 4. validate+sim │
│ │ ──eth_call───────> │
│ │ 5. send tx │
│ │ ──executeSwap────> │
│ │ [ATOMIC TX] │
│ │ withdraw() │
│ │ approve+swap() │
│ │ approve+deposit()│
│ │ [END TX] │
│ │ <── receipt ────── │
│ <── txHash ──────────────── │ │
│ │ │
│ 4. syncOnce() → new UTXO │ │
privacy-swap/
├── contracts/ # Solidity (Foundry)
│ ├── src/
│ │ ├── PrivacySwap.sol # Core atomic swap contract
│ │ └── interfaces/
│ │ ├── IOCash.sol # OCash withdraw/deposit interface
│ │ └── ISmartRouter.sol # PancakeSwap SmartRouter interface
│ ├── test/
│ │ ├── PrivacySwap.t.sol # 16 unit tests
│ │ └── mocks/ # MockOCash, MockSmartRouter, MockERC20
│ └── script/Deploy.s.sol # Deployment script
├── sdk/ # TypeScript SDK
│ └── src/
│ ├── PrivacySwapSDK.ts # Main class (getQuote, prepare, buildTransaction, submit)
│ ├── types.ts # All type definitions
│ ├── quote.ts # PancakeSwap routing logic
│ └── abi/PrivacySwap.ts # Contract ABI
└── relayer/ # Reference relayer (Fastify)
└── src/
├── handler.ts # POST/GET endpoints
├── validator.ts # Request validation
├── submitter.ts # Tx simulation + submission
└── config.ts # Environment configuration
cd contracts
forge install # Install dependencies (forge-std, OpenZeppelin)
forge build # Compile
forge test -vvv # Run all 16 testscd contracts
# Set environment variables
export OCASH_CONTRACT=0x... # OCash contract address on BSC
export SMART_ROUTER=0x13f4EA83D0bd40E75C8222255bc855a974568Dd4 # PancakeSwap SmartRouter (BSC)
export OWNER=0x... # Contract owner (multisig recommended)
export RELAYER=0x... # Relayer EOA address
# Deploy
forge script script/Deploy.s.sol:Deploy \
--rpc-url https://bsc-dataseed1.binance.org \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcastimport { createSdk } from '@ocash/sdk';
import { PrivacySwapSDK } from '@privacy-swap/sdk';
import { createPublicClient, http } from 'viem';
import { bsc } from 'viem/chains';
const publicClient = createPublicClient({ chain: bsc, transport: http() });
const ocashSdk = createSdk({ chains: [{ chainId: 56, /* ... */ }] });
await ocashSdk.core.ready();
const privacySwap = new PrivacySwapSDK(ocashSdk, {
privacySwapContract: '0x...', // Deployed PrivacySwap address
smartRouterAddress: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4',
relayerUrl: 'https://relayer.example.com',
}, publicClient);
// 1. Quote
const quote = await privacySwap.getQuote({
chainId: 56,
tokenInAssetId: 'usdt',
tokenOutAssetId: 'wbnb',
amountIn: 1000_000000n, // 1000 USDT (6 decimals)
slippageBps: 50, // 0.5%
});
// 2. Prepare (generates ZK proof, ~10-30s)
const keyPair = ocashSdk.keys.deriveKeyPair('my-secret-seed', '0');
const prepared = await privacySwap.prepare({
chainId: 56,
tokenInAssetId: 'usdt',
tokenOutAssetId: 'wbnb',
quote,
ownerKeyPair: keyPair,
depositSeed: 'fresh-deposit-seed', // New key for deposit (better privacy)
relayerAddress: '0x...',
relayerFee: 2_000000n, // 2 USDT relayer fee
});
// 3a. Submit via relayer
const result = await privacySwap.submit(prepared);
const txHash = await result.txHash;
// 3b. Or build calldata and submit yourself
const tx = privacySwap.buildTransaction(prepared);
// tx = { to, data, value: 0n, gasLimit: 700000n }cd relayer
pnpm install
cp .env.example .env # Edit with your config
# Environment variables:
# RELAYER_PRIVATE_KEY=0x...
# PRIVACY_SWAP_CONTRACT=0x...
# RPC_URL=https://bsc-dataseed1.binance.org
# CHAIN_ID=56
# PORT=3000
pnpm dev # Development mode| Operation | Gas |
|---|---|
| OCash withdraw (ZK proof verification) | ~200,000 |
| ERC-20 approve + PancakeSwap swap | ~195,000 |
| ERC-20 approve + OCash deposit | ~145,000 |
| Contract overhead | ~30,000 |
| Total | ~570,000 |
At 3 gwei on BSC: 0.0017 BNB ($1)
Hidden: User's EOA address, link between user and swap
Visible: Swap amount, token pair, timing, PrivacySwap contract address, BabyJubjub deposit public key
Recommendations:
- Use a fresh BabyJubjub key pair for the deposit (
depositSeedparameter) - Vary swap amounts to reduce correlation
- Use multiple relayers to avoid single-point censorship
- Atomicity: All three steps (withdraw, swap, deposit) in one transaction. Any failure reverts everything.
- ZK Proof Binding: The OCash ZK proof binds
recipientto the contract address. Relayer cannot redirect tokens. - Slippage Protection:
amountOutMinimumenforced on-chain by PancakeSwap SmartRouter. - Relayer Authorization: Only whitelisted relayers can call
executePrivacySwap. - Reentrancy Guard: OpenZeppelin
ReentrancyGuardon the core function. - MEV Protection: Relayer can use Flashbots Protect or private mempools.
| Component | Dependency | Purpose |
|---|---|---|
| Contract | OpenZeppelin 5.x | SafeERC20, ReentrancyGuard, Ownable |
| SDK | @ocash/sdk | ZK proof generation, UTXO management |
| SDK | @pancakeswap/smart-router | Swap routing and quoting |
| SDK | viem | Ethereum client, ABI encoding |
| Relayer | fastify | HTTP server |
| Relayer | viem | Transaction signing and submission |
MIT