Skip to main content

Documentation Index

Fetch the complete documentation index at: https://fhenix-mintlify-fix-broken-nav-1776644086.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Overview

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.

Wrapper Types

FHERC20 provides two wrapper contracts:
WrapperPurposeShield FunctionUnderlying
FHERC20ERC20WrapperShields ERC20 tokensshield(to, amount)Any standard ERC20
FHERC20NativeWrapperShields native tokensshieldNative(to) / shieldWrappedNative(to, value)Native currency (e.g., ETH) or WETH
Both wrappers share the same unshielding flow via the FHERC20WrapperClaimHelper.

Rate and Decimal Conversion

Wrappers normalize token decimals to fit within the euint64 type. The maximum confidential decimals default to 6 (configurable via _maxDecimals()).
// Conversion rate between underlying and confidential precision
function rate() external view returns (uint256);
For an ERC20 with 18 decimals:
  • rate() = 10^(18 - 6) = 10^12
  • Shielding 1,000,000,000,000 (1e12) underlying tokens produces 1 confidential token
  • Amounts are truncated to the nearest multiple of rate() during shielding
Some non-standard tokens such as fee-on-transfer or other deflationary-type tokens are not supported by the wrapper.

ERC20 Wrapper (FHERC20ERC20Wrapper)

How It Works

1

Shield Tokens

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).

Shielding Tokens

function shield(address to, uint256 amount) external returns (euint64);
Parameters:
  • to: Address to receive the shielded (confidential) tokens
  • amount: Amount of ERC20 tokens to shield (will be truncated to nearest multiple of rate())
Returns: The encrypted amount of confidential tokens minted.
// 1. Approve wrapper to spend your tokens
await erc20Token.approve(wrapperAddress, amount);

// 2. Shield tokens (receives confidential tokens)
await wrapper.shield(recipientAddress, amount);

// 3. Check confidential balance (encrypted)
const encBalance = await wrapper.confidentialBalanceOf(recipientAddress);

// 4. Transfer confidentially
const [encAmount] = await cofheClient
  .encryptInputs([Encryptable.uint64(100n)])
  .execute();
await wrapper.confidentialTransfer(anotherAddress, encAmount);

ERC1363 Direct Shielding

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);

Native Wrapper (FHERC20NativeWrapper)

The native wrapper shields native tokens (e.g., ETH) or WETH into confidential FHERC20 tokens.

Shield Native Tokens

// Shield native currency directly (e.g., ETH)
function shieldNative(address to) external payable returns (euint64);

// Shield WETH tokens
function shieldWrappedNative(address to, uint256 value) external returns (euint64);
Parameters:
  • to: Address to receive the shielded tokens
  • value: Amount of WETH to shield (for shieldWrappedNative)
  • msg.value: Amount of native currency to shield (for shieldNative)
Amounts are truncated to the nearest multiple of rate(). Any dust below the threshold is refunded to the caller.
// Shield ETH directly
await nativeWrapper.shieldNative(recipientAddress, { value: ethers.parseEther("1.0") });

// Or shield WETH
await weth.approve(nativeWrapperAddress, amount);
await nativeWrapper.shieldWrappedNative(recipientAddress, amount);

Unshielding Tokens

Unshielding is a three-step process shared by both wrapper types: burn on-chain, decrypt off-chain, then claim with proof.

Step 1: Unshield (Burn and Create Claim)

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:
  1. Burns the specified amount of confidential tokens from from
  2. Calls FHE.allowPublic(burned) so anyone can request decryption of the burned amount
  3. Creates a claim for the recipient
// Unshield 100 confidential tokens
await 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.

Step 2: Decrypt Off-Chain

Retrieve the claim’s ctHash using getUserClaims, then request decryption via decryptForTx. Since FHE.allowPublic was called, no permit is needed:
// Get the user's pending claims
const claims = await wrapper.getUserClaims(myAddress);
const claimCtHash = claims[0].ctHash;

// Request decryption off-chain (no permit needed)
const decryptResult = await client
  .decryptForTx(claimCtHash)
  .withoutPermit()
  .execute();

// decryptResult.decryptedValue — the plaintext amount
// decryptResult.signature      — the decryption proof

Step 3: Claim Unshielded 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 proof
const tx = await wrapper.claimUnshielded(
  decryptResult.ctHash,
  decryptResult.decryptedValue,
  decryptResult.signature
);
await tx.wait();

// Check underlying token balance
const balance = await erc20Token.balanceOf(myAddress);

Batch Claiming

You can claim multiple unshield requests in a single transaction:
function claimUnshieldedBatch(
    bytes32[] memory unshieldRequestIds,
    uint64[] memory unshieldAmountCleartexts,
    bytes[] memory decryptionProofs
) external;
// Batch claim all pending unshields
const claims = await wrapper.getUserClaims(myAddress);
const ids = [];
const amounts = [];
const proofs = [];

for (const claim of claims) {
    const result = await client
      .decryptForTx(claim.ctHash)
      .withoutPermit()
      .execute();
    ids.push(result.ctHash);
    amounts.push(result.decryptedValue);
    proofs.push(result.signature);
}

await wrapper.claimUnshieldedBatch(ids, amounts, proofs);

Claim Management

Claim Structure

struct Claim {
    address to;              // Recipient address
    bytes32 ctHash;          // Ciphertext hash identifying the claim
    uint64 requestedAmount;  // Original requested unshield amount
    uint64 decryptedAmount;  // Actual decrypted amount (set after claim)
    bool claimed;            // Whether underlying tokens have been claimed
}

Getting Claim Information

function getClaim(bytes32 ctHash) public view returns (Claim memory);
// Get claim info
const claim = await wrapper.getClaim(ctHash);

console.log(`To: ${claim.to}`);
console.log(`Requested: ${claim.requestedAmount}`);
console.log(`Decrypted: ${claim.decryptedAmount}`);
console.log(`Claimed: ${claim.claimed}`);

Getting User Claims

function getUserClaims(address user) public view returns (Claim[] memory);
Returns all pending (unclaimed) claims for a user:
// Get all pending claims
const claims = await wrapper.getUserClaims(myAddress);

console.log(`You have ${claims.length} pending claims`);

for (const claim of claims) {
    console.log(`Claim ${claim.ctHash} - requested: ${claim.requestedAmount}`);
}

Events

// Emitted when an unshield request is created
event Unshielded(address indexed to, euint64 indexed amount);

// Emitted when an unshield request is claimed
event 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);

Complete Examples

ERC20 Wrapper

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { FHERC20ERC20Wrapper } from "@fhenixprotocol/confidential-contracts/contracts/FHERC20/extensions/FHERC20ERC20Wrapper.sol";

// Deploy a wrapper for an existing ERC20 token
contract MyTokenWrapper is FHERC20ERC20Wrapper {
    constructor(IERC20 underlyingToken)
        FHERC20ERC20Wrapper(underlyingToken, "Shielded MTK", "sMTK")
    {}
}

Native Wrapper

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { FHERC20NativeWrapper } from "@fhenixprotocol/confidential-contracts/contracts/FHERC20/extensions/FHERC20NativeWrapper.sol";

// Deploy a wrapper for native ETH
contract ShieldedETH is FHERC20NativeWrapper {
    constructor(address wethAddress)
        FHERC20NativeWrapper(wethAddress, "Shielded ETH", "sETH")
    {}
}

Security Considerations

Token Compatibility

FHERC20ERC20Wrapper only works with standard ERC20 tokens. It will NOT work with:
  • Rebasing tokens (token balance changes automatically)
  • Fee-on-transfer tokens (tokens that charge fees)
  • Already-encrypted FHERC20 tokens
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 decryption
await wrapper.unshield(user.address, user.address, amount);

// 2. Get the claim's ctHash
const claims = await wrapper.getUserClaims(user.address);
const ctHash = claims[0].ctHash;

// 3. Decrypt off-chain
const decryptResult = await client
  .decryptForTx(ctHash)
  .withoutPermit()
  .execute();

// 4. Claim with proof
await 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 tokens
const encBalance = await wrapper.confidentialBalanceOf(user.address);

// Try to unshield 200
await wrapper.unshield(user.address, user.address, 200);

// Claim will give you 0 underlying tokens
await 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 = 1e12
const 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:
// Multiple unshields
await wrapper.unshield(user.address, user.address, 50);  // Claim 1
await wrapper.unshield(user.address, user.address, 30);  // Claim 2
await wrapper.unshield(user.address, user.address, 20);  // Claim 3

// Batch claim all at once
const claims = await wrapper.getUserClaims(user.address);
const ids = [], amounts = [], proofs = [];

for (const claim of claims) {
    const result = await client
      .decryptForTx(claim.ctHash)
      .withoutPermit()
      .execute();
    ids.push(result.ctHash);
    amounts.push(result.decryptedValue);
    proofs.push(result.signature);
}

await wrapper.claimUnshieldedBatch(ids, amounts, proofs);
Provide UI to track and manage multiple claims.

Use Cases

DEX Privacy

Shield tokens before trading on a confidential DEX, then unshield profits. Your trading activity and positions remain private.

Private Payments

Shield stablecoins for confidential payments, then unshield to cash out to bank accounts or fiat on-ramps.

Confidential Payroll

Companies can shield tokens, distribute salaries confidentially, and employees unshield to receive standard tokens.

Privacy Pools

Create pools where users deposit tokens for privacy, transact confidentially, and withdraw when desired.