Overview
ERC20Confidential combines a fully transparent ERC-20 with a confidential layer. That dual nature is powerful but introduces its own footguns: value straddles a public/private boundary, confidential mutations never revert, and encrypted handles change on every update. This page collects the practices that matter most for dual-mode tokens. The confidential-transfer, operator, and callback guidance from FHERC20 Best Practices applies here too.
Mind the Privacy Boundary
The single most important thing to understand about dual-mode: the public layer reveals everything, the confidential layer reveals nothing. Value only becomes private once it crosses into the confidential layer viashield.
Shielding and unshielding are public events
Shielding and unshielding are public events
shield() emits TokensShielded(account, amount) with a plaintext amount, and moves a visible public balance into CONFIDENTIAL_POOL. Anyone can see that you shielded and how much.The pool balance is a public aggregate
The pool balance is a public aggregate
CONFIDENTIAL_POOL’s public balance equals the total backing of all confidential balances. confidentialTotalSupply() derives directly from it. This is fine — it’s an aggregate — but remember it’s fully public and cannot be hidden.Design where users cross the boundary
Design where users cross the boundary
A DEX that only ever sees the public side of your token learns nothing about confidential holdings. Route sensitive flows through confidential transfers, and only unshield when the recipient genuinely needs public liquidity.
Backing and Minting
Every confidential unit must stay backed
Every confidential unit must stay backed
Confidential balances are only redeemable because
CONFIDENTIAL_POOL holds the corresponding public tokens. There are two correct ways to create confidential balances:Guard your mint functions
Guard your mint functions
ERC20Confidential is abstract and ships no minting policy. Add access control:Handle Zero-Replacement Correctly
Confidential transfers, unshields, and burns never revert on insufficient balance — they move encrypted zero instead. This preserves privacy but breaks any code that assumes the requested amount moved.Access Control on Encrypted Handles
Handles change on every update — re-grant access
Handles change on every update — re-grant access
Each confidential mutation stores a fresh
euint64 handle. FHE.allow grants on an old handle don’t carry over. _confidentialUpdate re-grants allowThis + allow(..., holder) automatically for balances and transferred amounts, but if your contract caches or re-exposes a handle, re-grant after every change.A handle of zero means 'never held', not 'zero balance'
A handle of zero means 'never held', not 'zero balance'
confidentialBalanceOf returns 0 for an account that has never had a confidential balance. Always check before decrypting:confidentialTotalSupply is not decryptable
confidentialTotalSupply is not decryptable
confidentialTotalSupply() returns a synthetic handle wrapping a plaintext number — it is not a registered ciphertext. Never attempt to decrypt it or feed it into FHE operations; read it as informational only.Operators
The operator guidance is identical to FHERC20 — operators get full, all-or-nothing access to a holder’s confidential balance until expiry.Prefer short windows
Grant
setOperator(spender, block.timestamp + 10 minutes) for a single interaction rather than type(uint48).max.Revoke by expiring now
setOperator(spender, uint48(block.timestamp)) revokes immediately — no separate revoke function needed.Use seconds, not milliseconds
until is a Unix timestamp in seconds. Use Math.floor(Date.now()/1000), never Date.now().Trust is total
An operator can move the holder’s entire confidential balance. Only authorize trusted contracts/addresses.
See Confidential Operations for signatures and FHERC20 Best Practices for the full operator playbook.
Choosing the Contract Variant
Constructor vs Upgradeable
Constructor vs Upgradeable
ERC20Confidential— immutable, constructor-based. Simpler, cheaper to deploy, no proxy. Use it when you don’t need upgradeability.ERC20ConfidentialUpgradeable— proxy-safe. No constructor; call__ERC20Confidential_init(name, symbol, decimals)from your initializer. State lives in an ERC-7201 namespaced struct to avoid storage-layout collisions across upgrades.
Pick your decimals deliberately
Pick your decimals deliberately
The public token can be 18 decimals to match mainstream ERC-20s, but the confidential layer caps at 6. That means sub-
rate() dust (e.g. sub-10^12 for an 18-decimal token) can never be shielded. If precise small amounts matter for your use case, consider a lower public decimals so rate() is smaller (or 1).Related Topics
- Overview — what dual-mode is and when to use it
- The Dual-Balance Model — pool, rate, and the update engine
- Shield & Unshield — the bridge between layers
- Confidential Operations — transfers, operators, callbacks
- FHERC20 Best Practices — shared confidential-token guidance