Skip to main content

Overview

The defining feature of ERC20Confidential is that every holder owns two balances simultaneously inside one contract: a public ERC-20 balance and a confidential (FHE-encrypted) balance. This page explains how those two layers are stored, how they stay backed 1:1, and how the encrypted bookkeeping engine (_confidentialUpdate) drives every confidential mutation.

The Two Layers

Public Layer

Inherited from OpenZeppelin ERC20Transparent balances tracked in the standard _balances mapping. balanceOf, transfer, approve, transferFrom, and totalSupply behave like any ERC-20. Fully visible on-chain.

Confidential Layer

mapping(address => euint64) _confidentialBalancesEncrypted balances stored as euint64 handles. Readable only by the holder (via ACL grant + permit) and the contract. Operated on entirely under FHE.
Because ERC20Confidential inherits the real OpenZeppelin ERC20, the public side is genuinely functional — this is the inversion of FHERC20, where those same functions revert.
On FHERC20, balanceOfIsIndicator() returns true and balanceOf returns a fake activity number. On ERC20Confidential, both signals declare the public balance real, and confidential-activity display is delegated to a separate sidecar token.

The Confidential Pool

CONFIDENTIAL_POOL is a fixed sentinel address (0x1011...0000) that custodies the public tokens backing every confidential balance. It is the linchpin of the dual-balance model.
1

Shielding deposits into the pool

When you shield, your public tokens are transferred to CONFIDENTIAL_POOL and an equal encrypted amount is credited to your confidential balance.
2

The pool holds the backing

At any moment, the public balance sitting at CONFIDENTIAL_POOL equals the total value of all confidential balances (scaled by the conversion rate).
3

Claiming withdraws from the pool

When you unshield and claim, public tokens are transferred out of CONFIDENTIAL_POOL back to you.
This backing invariant is why confidentialTotalSupply() can be derived directly from the pool’s public balance:
The euint64 returned by confidentialTotalSupply() is synthetic — it wraps a plaintext number, not a registered ciphertext. It is for display/inspection only; you cannot decrypt it or feed it into FHE operations.

Conversion Rate and Decimals

The public token may use any decimals; the confidential layer is capped at 6 to keep encrypted balances within euint64.
The rate governs both directions of the bridge:
  • Shielding rounds the public amount down to a whole multiple of the rate, then divides by the rate to get the confidential amount.
  • Claiming multiplies the decrypted confidential amount by the rate to compute the public payout.
Dust below one confidential unit cannot be shielded. shield() reverts with AmountTooSmallForConfidentialPrecision if the rounded amount is zero. See Shield & Unshield.

The Confidential Update Engine

_confidentialUpdate is the encrypted analog of ERC-20’s _update. Every confidential mutation — shield mint, confidential transfer, unshield burn — flows through it. Passing address(0) as from means mint; passing address(0) as to means burn.
Key behaviors baked into this function:

Zero-replacement on insufficient balance

FHESafeMath.tryDecrease returns an encrypted success flag. If the sender’s balance is too low, FHE.select(success, amount, 0) sets the transferred amount to encrypted zero instead of reverting.This is deliberate: reverting would leak whether the sender had enough balance. The trade-off is that transfers can silently move zero tokens — always work with the returned transferred handle, never the requested amount.

Fresh handle on every update

Each balance change stores a new euint64 handle (ptr). Any FHE.allow you granted on a previous handle no longer applies to the new one. The engine re-grants allowThis + allow(..., holder) on every update so the contract and holder retain access.

Access control is granted inline

After each mutation the function grants:
  • FHE.allowThis(...) on the new balance and the transferred amount — so the contract can keep operating on them.
  • FHE.allow(balance, holder) — so the holder can decrypt their own balance.
  • FHE.allow(transferred, from/to) — so both parties can see what moved.
Learn more in Access Control.

The Indicator Sidecar Token

On FHERC20 the token’s own balanceOf doubles as a wallet “activity indicator.” ERC20Confidential can’t do that — its balanceOf is a real balance. Instead, the constructor deploys a separate companion token, ERC20ConfidentialIndicator, dedicated to showing confidential activity in wallets and explorers without revealing real amounts.
Characteristics of the sidecar:
  • Name/symbol: "1011000 <ParentName>" / "c<PARENTSYMBOL>" (e.g. cMDT), with 4 decimals.
  • Fake balances: balanceOf returns 10110000000 + _indicatedBalances[account] — a deliberately non-revealing number that changes with activity but encodes no real amount.
  • Driven only by the parent: emitConfidentialTransfer(from, to) is onlyParent. The parent calls it inside _confidentialUpdate, nudging a bounded internal counter and emitting a Transfer(from, to, 10110000001) so explorers register that something happened.
  • Inert on its own: transfer, transferFrom, approve, and allowance all revert with ERC20ConfidentialIndicatorNoOp(). You cannot move or approve the indicator token — it exists purely for display.
The indicator’s counter jitters within bounds (increment seeds at 5001, capped at 9999; decrement seeds at 4999, floored at 1). These numbers are intentionally meaningless — they signal activity, not balance. Access the deployed sidecar via the parent’s public indicatorToken variable.

Reading Balances

A confidential balance handle of 0 means the account has never held a confidential balance — not that the balance is zero. Always check before attempting to decrypt.