EIP-8182 Private ETH and ERC-20 transfers
Hegota Declined for Inclusion · Security & privacy · EIP status Review · created 2026-03-03
Installs a shielded pool system contract managed directly by the protocol, so that ETH and ERC-20 tokens can be sent privately. It adds no new instructions or transaction types.
Why it is needed
Sending assets publicly on Ethereum is easy. But there is no common default for transfers that need to hide the sender, the recipient, and the amount. Ordinary financial activity such as payroll, corporate treasury management, and donations becomes problematic when the details are visible to everyone, so it is currently pushed to traditional finance or other blockchains. The source locates the reason the market has not solved this on its own in the scale effects of the pool. The anonymity of a private transfer depends on how many users and how much capital share the same pool, so a small pool has weak privacy no matter how good the product is. Also, if a pool is made upgradeable, whoever holds that authority can threaten user funds, and if it is made immutable instead, it cannot be fixed even when the proof system weakens. This EIP proposes to break this deadlock by providing a protocol-managed pool that changes only through hard forks.
What changes
Today
- There is no common standard for private transfers, so each app has to build and grow its own pool
- If a pool is small, privacy is weak even when the product is good
- An upgradeable pool carries operator risk, and an immutable pool cannot adapt to changes in cryptographic assumptions
After EIP-8182
- A system contract at a protocol-defined address becomes the shared shielded pool for ETH and ERC-20
- There is no proxy or admin, and the code changes only through hard forks
- Users register and use their own authentication method (ECDSA signature, passkey, etc.) directly
- No new precompile, opcode, or transaction type is needed, only the installation of a system contract
Who is affected
- Users: Transfers whose amounts or counterparties are hard to disclose, such as payroll, treasury management, and donations, become possible on Ethereum.
- Wallet & app developers: You build products on the shared pool without needing to grow and secure your own pool. However, supporting infrastructure such as note delivery and mempool encryption is outside the scope of this EIP, so you must provide it yourself.
- Client teams: The system contract code must be installed at the designated address at fork activation. The bytecode will be reflected in a release after the trusted setup is complete.
- Token issuers: Fee-on-transfer and rebasing tokens are not compatible with this pool and must not be deposited.
In depth
System contract and upgrade model
The shielded pool is deployed as a system contract at SHIELDED_POOL_ADDRESS = 0x0000000000000000000000000000000000081820. It has no proxy, no admin functions, and no on-chain upgrade path, and its code can be replaced only by the state transition rules of a later hard fork, while storage is preserved after replacement. The pool proof verification key is finalized through a one-time multi-party trusted setup ceremony, the same pattern as KZG in EIP-4844, and is embedded in the contract bytecode. Because the bytecode is only finalized once the ceremony ends, the EIP does not pin down a specific bytecode.
Split proof structure: pool proof and auth proof
A transact that spends notes is split into two proofs with different trust characteristics. The pool proof is a Groth16 BN254 SNARK of a single circuit that changes only through hard forks, and it is responsible for all protocol invariants: value conservation, nullifier derivation, Merkle membership, deterministic derivation of output note secrets, registry leaf and policy set membership, and recomputation of the intent digest. The auth proof is a permissionless layer where anyone can build and deploy a circuit and an authVerifier contract. It handles user credential verification and intent parsing, and outputs two values, [blindedAuthCommitment, transactionIntentDigest]. The system contract verifies the pool proof with the embedded verification key and passes the auth proof to the authVerifier chosen by the user via staticcall. A bug in the pool circuit threatens the funds of the entire pool, but a bug in an auth circuit threatens only the identities that registered a policy with that verifier.
Notes, nullifiers, and fixed-shape transactions
A note is committed through a Poseidon hash chain. ownerCommitment = poseidon(OWNER_COMMITMENT_DOMAIN, ownerNullifierKeyHash, noteSecret), noteBodyCommitment binds amount and tokenAddress to it, and the final noteCommitment adds the leafIndex assigned by the contract to create structural uniqueness. When a note is spent, nullifier = poseidon(NULLIFIER_DOMAIN, noteCommitment, ownerNullifierKey) is made public to prevent double spending. Every transact has a fixed shape of 2 inputs and 3 outputs. If there is only one real input, a phantom input fills the empty slot and emits a phantom nullifier, and the remaining output slots are filled with dummy outputs whose amount is forced to 0. An observer must not be able to tell phantom from real. All hashes are unified as a length-tagged sponge construction of Poseidon2 (t=4, R_F=8, R_P=56) over the BN254 scalar field.
Auth policy registry and key rotation
A single sparse Merkle tree of depth 32 holds the auth policies of all users. Each leaf is poseidon(AUTH_POLICY_DOMAIN, uint160(user), ownerNullifierKeyHash, noteSecretSeedHash, policySetCommitment), and each address is permanently assigned one leafPosition. Each individual policy is hidden in a tree of depth POLICY_SET_DEPTH = 8 as policyCommitment = poseidon(POLICY_COMMITMENT_DOMAIN, uint160(authVerifier), authDataCommitment, registrationBlinder), so at registration time it is not revealed which address uses which auth method (authVerifier is revealed as a public input at spend time). ownerNullifierKeyHash cannot be rotated, so changing it requires registering a new identity under a new address and transferring the notes. noteSecretSeedHash and the policy set can be changed with setAuthPolicy, but the previous state stays valid until the root history window of AUTH_POLICY_ROOT_HISTORY_BLOCKS = 64 blocks has passed, so a rotation does not take effect immediately.
How transact executes
The contract takes 19 public inputs and checks them in order. It verifies chain ID match, intent expiry (validUntilSeconds must be within block.timestamp + MAX_INTENT_LIFETIME = 86400 seconds), the note commitment root (the current root or a history of size NOTE_COMMITMENT_ROOT_HISTORY_SIZE = 500), the auth policy root, nullifier duplication, and range checks (publicAmountOut < 2^248, address-type values < 2^160). It then verifies the pool proof with the embedded key and the auth proof via a staticcall to authVerifier. Next it marks the 2 nullifiers and the intentReplayId as consumed, and compares the keccak256 mod p hash of the 3 output note data items against the public inputs. If publicAmountOut > 0, it is a withdrawal and ETH or ERC-20 is paid out publicly; if it is 0, the transfer is purely private. Finally it inserts the 3 output commitments into the tree and emits a ShieldedPoolTransact event.
Deposits without proofs and ERC-20 compatibility limits
deposit works without a proof. For ETH it checks msg.value == amount, and for ERC-20 it checks that the balanceOf difference before and after transferFrom is exactly amount, then inserts one note. The contract does not verify that ownerCommitment corresponds to a registered address, and it does not interpret outputNoteData. The flow is that the sender learns the recipient's ownerNullifierKeyHash off-chain and computes ownerCommitment together with noteSecret. Because of the balance difference check, fee-on-transfer tokens are rejected at deposit, rebasing tokens cannot be detected reliably, and tokens that take a fee only on the withdrawal (transfer) side pass at deposit but deliver less at withdrawal, so they must not be deposited. Note delivery, mempool encryption, network anonymity, and wallet integration remain outside the scope of this EIP.
Status: Review, declined for inclusion in Hegotá
This EIP is in Review status, so the specification may still change. In the core developer calls, it was decided at ACDE #245 on 09.10 to decline it (DFI) from the Hegotá fork candidates. Together with EIP-2488, EIP-8219, and EIP-7645, all four received unanimous low ratings (0.0 to 0.8) from every client.