Skip to main content

Overview

FHERC20Wrapper enables you to convert standard ERC20 tokens 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 balances into encrypted FHERC20 balances for confidential transactions.

Reversible

Unwrap confidential tokens back to standard ERC20 at any time through a secure claim process.

1:1 Backing

Each wrapped token is backed 1:1 by the underlying ERC20 token held in the wrapper contract.

DeFi Bridge

Bridge between transparent DeFi protocols and confidential trading/transfers.

How It Works

1

Wrap Tokens

User deposits standard ERC20 tokens into the wrapper contract, which mints an equivalent amount of confidential FHERC20 tokens.
2

Confidential Transfers

User can now transfer the wrapped tokens confidentially using all FHERC20 features while balances remain encrypted.
3

Unwrap 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 Threshold Network signature.
5

Claim Tokens

The user submits the plaintext and signature on-chain via claimUnwrapped. The contract verifies the proof and transfers the ERC20 tokens.

Wrapping Tokens

Function Signature

function wrap(address to, uint64 value) external;
Parameters:
  • to: Address to receive the wrapped (confidential) tokens
  • value: Amount of ERC20 tokens to wrap

How Wrapping Works

  1. User approves wrapper contract to spend ERC20 tokens
  2. Wrapper transfers ERC20 tokens from user to itself
  3. Wrapper mints equivalent confidential FHERC20 tokens to recipient
  4. Wrapped tokens can now be used confidentially

Example

// 1. Approve wrapper to spend your tokens
await erc20Token.approve(wrapperAddress, amount);

// 2. Wrap tokens (receives confidential tokens)
await wrapper.wrap(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);

Unwrapping Tokens

Unwrapping is a three-step process: burn on-chain, decrypt off-chain, then claim with proof.

Step 1: Unwrap (Burn and Allow Public Decryption)

function unwrap(address to, uint64 value) public;
Parameters:
  • to: Address to receive the unwrapped ERC20 tokens (defaults to msg.sender if address(0))
  • value: Amount of confidential tokens to unwrap
This function:
  1. Burns the specified amount of confidential tokens from the caller
  2. Calls FHE.allowPublic(burned) so anyone can request decryption of the burned amount
  3. Creates a claim for the recipient
// Unwrap 100 tokens
await wrapper.unwrap(myAddress, 100);

// A claim is created, but ERC20 tokens aren't sent yet
// The burned amount is now publicly decryptable
Due to the zero-replacement behavior, if you attempt to unwrap 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 claimableCtHash = claims[0].ctHash;

// Optionally inspect the claim details
const claim = await wrapper.getClaim(claimableCtHash);

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

// decryptResult.decryptedValue — the plaintext amount
// decryptResult.signature      — the Threshold Network signature

Step 3: Claim Unwrapped Tokens

Submit the plaintext and signature to the contract. The contract verifies the proof and transfers the ERC20 tokens:
function claimUnwrapped(bytes32 ctHash, uint64 decryptedAmount, bytes memory decryptionSignature) public;
Parameters:
  • ctHash: The ciphertext hash identifying the claim
  • decryptedAmount: The plaintext value returned by decryptForTx
  • decryptionSignature: The Threshold Network signature proving the plaintext is authentic
// Claim your ERC20 tokens by submitting the proof
const tx = await wrapper.claimUnwrapped(
  decryptResult.ctHash,
  decryptResult.decryptedValue,
  decryptResult.signature
);
await tx.wait();

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

Claim Management

Getting Claim Information

function getClaim(bytes32 ctHash) public view returns (Claim memory);
Returns claim details:
struct Claim {
    bytes32 ctHash;          // Ciphertext hash identifying the claim
    uint64 requestedAmount;  // Original requested unwrap amount
    uint64 decryptedAmount;  // Actual decrypted amount (set after claim)
    bool decrypted;          // Whether the claim has been fulfilled
    address to;              // Recipient address
    bool claimed;            // Whether ERC20 tokens have been claimed
}
// 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(`Fulfilled: ${claim.decrypted}`);
console.log(`Claimed: ${claim.claimed}`);

Getting User Claims

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

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

// Find claims ready to be fulfilled
for (const claim of claims) {
    if (!claim.claimed) {
        console.log(`Claim ${claim.ctHash} can be claimed`);
    }
}

Complete Example

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

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

// Deploy a wrapper for an existing ERC20 token
contract MyTokenWrapper is FHERC20Wrapper {
    constructor(IERC20 underlyingToken)
        FHERC20Wrapper(underlyingToken, "eMTK")
    {}
}

Symbol Management

The wrapper owner can update the token symbol:
function updateSymbol(string memory updatedSymbol) external onlyOwner;
This is useful for:
  • Distinguishing wrapped versions (e.g., “wUSDC” vs “USDC”)
  • Rebrand if needed
  • Fix initial symbol mistakes
// Update symbol
await wrapper.updateSymbol("wUSDC");

// Verify
const symbol = await wrapper.symbol();
console.log(`New symbol: ${symbol}`); // "wUSDC"

Security Considerations

Token Compatibility

FHERC20Wrapper 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)
  • Tokens with transfer hooks
  • 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. Unwrap burns tokens and allows public decryption
await wrapper.unwrap(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.claimUnwrapped(
  decryptResult.ctHash,
  decryptResult.decryptedValue,
  decryptResult.signature
); // ✅ Success
Implement proper UI feedback for the decryption step.

Zero-Replacement

If you unwrap more than your balance, you get zero:
// Balance: 100 confidential tokens
const encBalance = await wrapper.confidentialBalanceOf(user.address);

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

// Claim will give you 0 ERC20 tokens
await waitAndClaim(ctHash);
const received = 0; // Not 100, not 200, but 0
Always ensure sufficient balance before unwrapping.

Claim Management

Users can accumulate multiple pending claims. Each must be claimed individually with its own decryption proof:
// Multiple unwraps
await wrapper.unwrap(user.address, 50);  // Claim 1
await wrapper.unwrap(user.address, 30);  // Claim 2
await wrapper.unwrap(user.address, 20);  // Claim 3

// Get all claims
const claims = await wrapper.getUserClaims(user.address);
console.log(`${claims.length} pending claims`);

// Decrypt and claim each one
for (const claim of claims) {
    if (!claim.claimed) {
        const result = await client
          .decryptForTx(claim.ctHash)
          .withoutPermit()
          .execute();
        await wrapper.claimUnwrapped(
          result.ctHash,
          result.decryptedValue,
          result.signature
        );
    }
}
Provide UI to track and manage multiple claims.

Use Cases

DEX Privacy

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

Private Payments

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

Confidential Payroll

Companies can wrap tokens, distribute salaries confidentially, and employees unwrap to receive standard tokens.

Privacy Pools

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