Penguin Protocol
prevent your coworkers from stalking your paycheck
Created on 14th March 2026
•
Penguin Protocol
prevent your coworkers from stalking your paycheck
The problem Penguin Protocol solves
Penguin Protocol
Private payroll on Ethereum. Companies pay employees without exposing salary amounts or wallet addresses on-chain.
The Problem
Standard on-chain payroll leaks everything — sender, receiver, amount. Employees can be targeted, salaries compared, treasury movements tracked.
How It Works
Identity via ENS
Companies own
company.eth
. When they onboard an employee, they mint a subdomain (alice.company.eth
) directly to the employee's wallet usingNameWrapper.setSubnodeRecord
. No custom registry — ENS does the work.Authentication via SIWE
Both parties sign in with their wallet (EIP-4361). Backend verifies the signature, checks ENS subdomain ownership against the database, issues a JWT.
Contracts stored on Fileverse
Salary terms are encrypted with both the company's and employee's public keys (ECIES), stored as a JSON blob inside a markdown file on Fileverse (pinned to IPFS). Only the two parties can decrypt. The employee decrypts by signing a deterministic message — no private key input needed.
Private payroll via ShieldVault
The vault holds USDC and maintains a MiMC Merkle tree. The company deposits a batch of commitments — each one encodes a salary note against a stealth address. The employee submits a ZK proof (Noir + Barretenberg UltraHonk) proving membership in the tree and receives USDC at a stealth address with no on-chain link to their identity.
Why This Is Better
| Without this | With this |
|---|---|
| Salary visible on-chain | Encrypted in IPFS, ZK proof to claim |
| Employee wallet linked to payment | Stealth address, no on-chain link |
| Custom employee registry | ENS subdomains |
| Private key for decryption | Wallet signature derives key locally |
| Single point of failure | BitGo treasury + private vault separated |
Deployed (Base Sepolia)
| Contract | Address |
|---|---|
| MockUSDC | 0x231E63e5E40E208D7570aaD33eF8a045d8EA4A3d |
| HonkVerifier | 0xDA559F68d4D001E34a6ccDD55B2975E3eaD8d79B |
| ShieldVault | 0x367707c3710514B196Bcf6bafE11977e264aa223 |
ENS on Sepolia. Vault + ZK verification on Base Sepolia.
Stack
Next.js · RainbowKit/Wagmi · SIWE · ENS NameWrapper · Fileverse · Pinata/IPFS · Noir · Barretenberg · Solidity/Foundry · BitGo · Supabase
Challenges we ran into
Bugs & Hurdles
1. ENS NameWrapper:
owner()
never returns your walletEvery
.eth
name registered via the ENS app since 2023 is "wrapped". The ENS Registry'sowner(node)
returns the NameWrapper contract, not your wallet. Our login check was callingregistry.owner(namehash(name))
— it always failed.Fix: check
NameWrapper.ownerOf(uint256(namehash(name)))
for wrapped names. Same issue hit subdomain creation —registry.setSubnodeOwner
is a no-op on wrapped names. Switched toNameWrapper.setSubnodeRecord(parentNode, stringLabel, owner, resolver, fuses, expiry)
. Also had to add an explicitgas: BigInt(300_000)
because viem's estimator returned 21M gas (network cap is 25M, but the estimate was wrong).2. MiMC constants identical in code, different outputs on-chain
The ZK circuit (Noir) and the Solidity contract both implement MiMC hash. Constants were copy-pasted — byte identical. Proofs passed locally but
verifier.verify()
always returned false on-chain.Root cause: the Noir
h([a, b, 0, 0])
function runs 3 rounds per input (12 rounds total). An early Solidity version ran 1 round per input (4 rounds total). The loop body looked similar, just missing two lines. Merkle roots computed by the contract and by the circuit diverged completely.Fix: wrote a Foundry test that printed intermediate values, ran
nargo test --show-output
to get the Noir equivalents, diffed step by step until both matched.3. SIWE chainId locked auth to one chain, txns needed the other
ENS is on Sepolia. ShieldVault is on Base Sepolia. SIWE embeds
chainId
in the signed message. When the wallet was on Base Sepolia, auth rejected because the backend expected Sepolia's chain ID. When it was on Sepolia, vault transactions failed because the wrong network was active.Fix: removed
chainId
enforcement from SIWE verification — the domain, nonce, and timestamp are sufficient for replay protection. Auth is about wallet identity, not network. Both chains are always in the Wagmi config; the frontend callsswitchChainAsync({ chainId: baseSepolia.id })
before any vault transaction without triggering re-auth.4. Contract decryption: can't ask users to paste private keys
Employment contracts are encrypted with ECIES. The obvious path is "user pastes their private key to decrypt". That's not acceptable.
Fix: deterministic key derivation from a wallet signature. The employee signs a fixed domain-scoped message (
penguin-protocol:decrypt:<address>
), the signature bytes are hashed with keccak256, and that 32-byte value is used as key material for AES-256-GCM via Web Crypto API. Same wallet, same key, every time. Nothing stored, nothing transmitted.5.
depositBatch
gas estimate: 131M, block limit: 25MShieldVault.depositBatch
inserts leaves into a MiMC Merkle tree. Viem estimated 131,250,000 gas for 6 leaves. Base Sepolia's limit is 25M. Transaction rejected before hitting the mempool.Fix:
gasLimit: 8_000_000
explicitly on the call. Actual cost on-chain was ~4.2M. Viem's simulation diverged from actual execution because the tree state at simulation time didn't match the real state.6. Fileverse
fileId
is aBigInt
,JSON.stringify
throwsagent.create()
returnsfileId
as a JavaScriptBigInt
(from an on-chain event). Passing it toJSON.stringify
or Supabase directly throwsTypeError: Do not know how to serialize a BigInt
. Contracts were being stored withfileverse_file_id: null
.Fix:
String(file.fileId)
before any serialization or DB write. One line, half a day lost.Tracks Applied (7)
Privacy
Best creative use of ENS
Ethereum Name Service
Pool prize
Ethereum Name Service
Best Privacy Application using BitGo
BitGo
Build What Big Tech Won't
Fileverse
Privacy
Base
BEST Overall Project
Technologies used
Cheer Project
Cheering for a project means supporting a project you like with as little as 0.0025 ETH. Right now, you can Cheer using ETH on Arbitrum, Optimism and Base.