A secure ERC-4337 paymaster implementation that sponsors gas fees for Privacy Pool withdrawal operations while ensuring only valid transactions are executed. This enables decentralized Privacy Pool withdrawals without relying on centralized relayer services.
The SimplePrivacyPoolPaymaster sponsors user operations for Privacy Pool withdrawals. It validates the entire withdrawal flow before sponsoring, guaranteeing transaction success and preventing gas waste.
The paymaster integrates with three core contracts:
- Privacy Pool Entrypoint: Main relay contract that processes withdrawals
- ETH Privacy Pool: The privacy pool where funds are deposited and stored
- Withdrawal Verifier: Groth16 verifier for zero-knowledge proofs
The paymaster uses a deterministic smart account approach for enhanced security and user experience:
- Single Expected Account: Only accepts UserOperations from a pre-configured smart account address
- Pre-deployment Required: Smart account must be deployed before withdrawal operations (prevents deployment cost charging)
- Recipient-based Refunds: Refunds go directly to the recipient specified in RelayData, not the smart account
- Cost Predictability: Users only pay for withdrawal transactions, not account deployment
The paymaster performs a 7-step validation process:
- Configuration Check - Ensures expected smart account is configured
- Sender Validation - Verifies UserOperation comes from expected smart account
- Deployment Check - Ensures smart account is already deployed (no initCode)
- Gas Limit Check - Validates sufficient post-operation gas limit
- CallData Validation - Direct extraction of SimpleAccount.execute() parameters
- ZK Proof Verification - Verifies zero-knowledge proofs and Privacy Pool state consistency
- Economic Check - Ensures relay fees cover gas costs and paymaster receives payment
The paymaster owner can configure the expected smart account:
function setExpectedSmartAccount(address account) external onlyOwnerThe paymaster charges approximately the gas cost of UserOperation execution and refunds any excess funds received (as part of relay fees) back to the recipient address. It only sponsors operations for pre-deployed accounts to protect users from unexpected deployment costs.
- Node.js (v18+)
- Foundry
- TypeScript
- Docker (for E2E testing)
# Clone with submodules or initialize them if already cloned
git submodule update --init --recursive
# Install dependencies
npm installnpm run build
# or
forge buildnpm test
# or
forge testDeploy to a local fork of Base mainnet:
npm run deployThis command will:
- Start an Anvil fork of Base mainnet
- Deploy the paymaster contracts using Forge
- Clean up the local node
Run the complete E2E test flow with mock AA environment:
cd mock-aa-environment
docker compose up -d
cd ..
npm run e2eCopy environment template:
cp .env.example .envThis implementation demonstrates using SimpleAccount from the @account-abstraction/contracts repository. The code can be easily modified to support other account implementations like Biconomy, Kernel, or Safe or other more gas-efficient account:
import { createSmartAccountClient } from "permissionless";
import { privateKeyToAccount } from "viem/accounts";
import { simpleSmartAccount } from "permissionless/accounts";
// Create SimpleAccount (deterministic address)
const simpleAccount = await simpleSmartAccount({
client: publicClient,
entryPoint: ENTRYPOINT_ADDRESS,
owner: privateKeyToAccount("0x..."), // publically know hardhat private key #0 used in withdrawal scripts
});
// Configure paymaster in your smart account client
const smartAccountClient = createSmartAccountClient({
account: simpleAccount,
entryPoint: ENTRYPOINT_ADDRESS,
chain: baseSepolia,
bundlerTransport: http("https://api.pimlico.io/v2/base-sepolia/rpc"),
paymaster: {
async getPaymasterStubData() {
return {
paymaster: PAYMASTER_ADDRESS,
paymasterData: "0x",
paymasterPostOpGasLimit: 32000n,
};
},
async getPaymasterData() {
return {
paymaster: PAYMASTER_ADDRESS,
paymasterData: "0x", // Paymaster validates via callData
paymasterPostOpGasLimit: 32000n,
};
}
}
});
// Execute privacy pool withdrawal
await smartAccountClient.sendUserOperation({
calls: [{
to: PRIVACY_POOL_ENTRYPOINT,
data: relayCallData, // Privacy pool withdrawal call
}],
});Adapting for Other Account Types:
- Biconomy Smart Account, Kernel Account, Safe Account or Custom Accounts : Update callData parsing in
_extractExecuteCall()for that account's execute format
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
We welcome contributions to improve privacy-preserving gas sponsorship! Here's how you can help:
- π Bug Reports: Submit issues with detailed reproduction steps
- π‘ Feature Requests: Suggest improvements for better privacy or efficiency
- π§ Code Contributions: Submit PRs with tests and documentation
- π Documentation: Help improve guides, examples, and explanations
- π§ͺ Testing: Add test cases, especially edge cases and gas optimization scenarios
- π¬ Discussions: Use GitHub Discussions for questions and ideas
- π¦ Twitter: Follow @kdsinghsaini for updates
Built with β€οΈ for Privacy, Ethereum, ERC-4337 and Privacy Pools by Karandeep Singh X/Twitter Telegram