Skip to main content
A wrapper turns a public asset into an FHERC20 token (a token whose balances are encrypted) backed one to one by what the wrapper holds. FHERC20ERC20Wrapper wraps any standard ERC-20. FHERC20NativeWrapper wraps ETH, taken directly or as WETH. Use this page to deploy a wrapper, shield into it, and get the underlying asset back out.

Deploy a wrapper

Both wrappers are abstract. You inherit one and pass the FHERC20 constructor arguments next to the wrapper’s own:
ConfidentialUSDC.sol
ConfidentialETH.sol
The native wrapper takes the WETH address even if you only use shieldNative, because it reads its decimals from WETH. The 6 passed to FHERC20 does not set the wrapper’s decimals. The wrapper overrides decimals() with a value derived from the underlying token. The constructor argument only sets the indicator tick, the unit that balanceOf reports in. Pass the wrapper’s real decimals, which is 6 for any underlying with 6 or more. Both wrappers call into ERC20ConfidentialLib, so you must deploy that library and link it into the wrapper. See link the shared library for the steps.
Enable the Solidity optimizer for wrappers. With the optimizer off, ConfidentialETH compiles to 24,693 bytes and an upgradeable ERC-20 wrapper to 26,751 bytes. Both exceed the 24,576-byte limit and fail to deploy. With runs: 200 they compile to 14,839 and 15,730 bytes.

How decimals and the rate work

Encrypted amounts are euint64, so a wrapper caps its own decimals at 6. When the underlying has more, the wrapper converts at a fixed rate(): One confidential unit is backed by rate() units of the underlying. Shielding rounds down to a multiple of rate(), and a claim pays amount * rate(). If the underlying has no decimals() function, the ERC-20 wrapper assumes 18.

Shield an ERC-20

shield(to, amount) pulls the underlying from the caller and mints confidential tokens to to. Approve the wrapper first:
The amount shielded is public: it is a plaintext argument and an ERC-20 transfer. Only the balances that follow are confidential.
  • The wrapper pulls amount - (amount % rate()) and mints amount / rate(). The remainder never leaves your account.
  • An amount below rate() mints zero and pulls nothing. It does not revert.
  • to cannot be address(0). The mint reverts with FHERC20InvalidReceiver.

Shield in one transaction with ERC-1363

If the underlying implements ERC-1363, you can skip the approval. Call transferAndCall on the underlying with the wrapper as the recipient. The wrapper’s onTransferReceived mints to the address in the first 20 bytes of data, or to the sender when data is shorter than 20 bytes:
Here the full amount arrives first, so the wrapper refunds the part below a multiple of rate() to the sender. onTransferReceived reverts with FHERC20UnauthorizedCaller unless the underlying token calls it.

Shield the native token

FHERC20NativeWrapper has two entry points:
  • shieldNative(to) takes msg.value. It refunds the part below a multiple of rate() to the caller.
  • shieldWrappedNative(to, value) pulls WETH, which you approve first, and unwraps it. The part below a multiple of rate() is never pulled.
Unlike the ERC-20 wrapper, both functions treat to == address(0) as the caller, and both revert with AmountTooSmallForConfidentialPrecision when the value is below rate(). With 18-decimal ETH the rate is 10^12, so 1 ETH mints 1,000,000 units.

Unshield back to the underlying

Getting the underlying back takes two transactions, because the amount is encrypted and the wrapper can only pay out a plaintext:
1

Unshield

Call unshield(from, to, amount). The wrapper burns the amount from from, makes the burned amount publicly decryptable, and records a claim for to.
2

Decrypt

Read the claim and decrypt its handle with decryptForTx. No ACP (Access Control Permission) is needed, because the burned amount is public.
3

Claim

Call claimUnshielded(id, value, signature). The wrapper verifies the decryption and sends value * rate() of the underlying to the claim’s recipient.
There are two unshield overloads: The caller must be from or an operator for from, or the call reverts with FHERC20UnauthorizedSpender. to cannot be address(0). If the amount exceeds the balance, the wrapper burns zero, and the claim decrypts to 0.

Find and settle the claim

A claim is keyed by a claim ID, not by the ciphertext handle. Two unshields can burn values with the same handle, so the handle cannot identify a claim. Read pending claims with getUserClaims(to), then decrypt ctHash and claim by id:
Any account can submit the claim. The payout always goes to the claim’s to, so a relayer can settle claims for your users. claimUnshieldedBatch(ids, values, signatures) settles several in one transaction and reverts as a whole if any element is invalid. See decrypt to transaction for the SDK side. getUserClaims returns only pending claims. getClaim(id) returns any claim as this struct:

Total supply checks

Because the wrapper’s holdings are public, it can bound its own supply. inferredTotalSupply() returns the underlying balance divided by rate(), and every mint reverts with FHERC20TotalSupplyOverflow if that exceeds maxTotalSupply(), which is type(uint64).max. Anyone can raise inferredTotalSupply() by sending the underlying to the wrapper directly. It also lags confidentialTotalSupply() between an unshield and its claim.
The ERC-20 wrapper assumes it receives the full amount it pulls. Fee-on-transfer and other deflationary tokens are not supported: the wrapper would mint more than it holds.

Errors

InvalidSigner comes from the TaskManager, which verifies the decryption. See common errors for the other TaskManager errors.

Events

Shielding also emits the FHERC20 Transfer and ConfidentialTransfer events for the mint. The ERC-20 wrapper has no shield event of its own.