FHERC20 Wrappers enable you to convert standard ERC20 tokens or native tokens (e.g., ETH) into confidential FHERC20 tokens and vice versa. This creates a privacy layer on top of existing tokens, allowing users to transact privately while maintaining interoperability with the broader DeFi ecosystem.
Privacy Layer
Transform transparent ERC20 or native token balances into encrypted FHERC20 balances for confidential transactions.
Reversible
Unshield confidential tokens back to standard ERC20 or native tokens at any time through a secure claim process.
1:1 Backing
Each shielded token is backed by the underlying token held in the wrapper contract, with a rate-based conversion for decimal normalization.
DeFi Bridge
Bridge between transparent DeFi protocols and confidential trading/transfers.
User deposits standard ERC20 tokens into the wrapper contract, which mints an equivalent amount of confidential FHERC20 tokens (divided by the rate).
2
Confidential Transfers
User can now transfer the shielded tokens confidentially using all FHERC20 features while balances remain encrypted.
3
Unshield Request
When ready to exit, user burns confidential tokens. The burned amount is marked as publicly decryptable via FHE.allowPublic, and a claim is created.
4
Decrypt Off-Chain
The user (or anyone) requests decryption of the burned amount off-chain via decryptForTx, receiving the plaintext and a decryption proof.
5
Claim Tokens
The user submits the plaintext and proof on-chain via claimUnshielded. The contract verifies the proof and transfers the underlying ERC20 tokens (multiplied by the rate).
The ERC20 wrapper also implements the ERC1363 onTransferReceived callback, allowing users to shield tokens in a single transaction without a separate approval step:
// Direct transfer-and-shield in one transaction (if the ERC20 supports ERC1363)await erc20Token.transferAndCall(wrapperAddress, amount, encodedRecipient);
function unshield(address from, address to, uint64 amount) external returns (euint64);
Parameters:
from: Address whose confidential tokens to burn (caller must be from or an operator for from)
to: Address to receive the underlying tokens after claiming
amount: Amount of confidential tokens to unshield
This function:
Burns the specified amount of confidential tokens from from
Calls FHE.allowPublic(burned) so anyone can request decryption of the burned amount
Creates a claim for the recipient
// Unshield 100 confidential tokensawait wrapper.unshield(myAddress, myAddress, 100);// A claim is created, but underlying tokens aren't sent yet// The burned amount is now publicly decryptable
Due to the zero-replacement behavior, if you attempt to unshield more than your balance, zero tokens will be burned and you’ll have a claim for zero tokens.
Submit the plaintext and proof to the contract. The contract verifies the proof via FHE.verifyDecryptResult and transfers the underlying tokens (multiplied by the rate):
function claimUnshielded( bytes32 unshieldRequestId, uint64 unshieldAmountCleartext, bytes calldata decryptionProof) external;
Parameters:
unshieldRequestId: The ciphertext hash identifying the claim
unshieldAmountCleartext: The plaintext value returned by decryptForTx
decryptionProof: The proof proving the plaintext is authentic
// Claim your underlying tokens by submitting the proofconst tx = await wrapper.claimUnshielded( decryptResult.ctHash, decryptResult.decryptedValue, decryptResult.signature);await tx.wait();// Check underlying token balanceconst balance = await erc20Token.balanceOf(myAddress);
// Emitted when an unshield request is createdevent Unshielded(address indexed to, euint64 indexed amount);// Emitted when an unshield request is claimedevent ClaimedUnshielded( address indexed to, bytes32 indexed unshieldRequestId, euint64 indexed unshieldAmount, uint64 unshieldAmountCleartext);// Emitted when native tokens are shielded (NativeWrapper only)event ShieldedNative(address indexed from, address indexed to, uint256 value);
Always test with the specific token before deploying to production.
Claim Flow
Claiming requires completing the off-chain decryption step first:
// 1. Unshield burns tokens and allows public decryptionawait wrapper.unshield(user.address, user.address, amount);// 2. Get the claim's ctHashconst claims = await wrapper.getUserClaims(user.address);const ctHash = claims[0].ctHash;// 3. Decrypt off-chainconst decryptResult = await client .decryptForTx(ctHash) .withoutPermit() .execute();// 4. Claim with proofawait wrapper.claimUnshielded( decryptResult.ctHash, decryptResult.decryptedValue, decryptResult.signature); // ✅ Success
Implement proper UI feedback for the decryption step.
Zero-Replacement
If you unshield more than your balance, you get zero:
// Balance: 100 confidential tokensconst encBalance = await wrapper.confidentialBalanceOf(user.address);// Try to unshield 200await wrapper.unshield(user.address, user.address, 200);// Claim will give you 0 underlying tokensawait waitAndClaim(ctHash);const received = 0; // Not 100, not 200, but 0
Always ensure sufficient balance before unshielding.
Rate Conversion
Be aware of the rate conversion when shielding and unshielding:
// For an ERC20 with 18 decimals, rate = 1e12const rate = await wrapper.rate();// Shielding 1.5 tokens (1.5e18 wei)// → 1.5e18 / 1e12 = 1,500,000 confidential units (1.5 with 6 decimals)await wrapper.shield(user.address, ethers.parseUnits("1.5", 18));// Amounts not aligned to rate are truncated// Shielding 1.5000005e18 → only 1.5e18 is shielded
Claim Management
Users can accumulate multiple pending claims. Use batch claiming for efficiency: