For the complete documentation index, see llms.txt. This page is also available as Markdown.

Confidential wrapper

This document gives an overview of the Confidential Wrapper, a smart contract that wraps standard ERC-20 tokens into confidential ERC-7984 tokens. Built on Zama's FHEVM, it enables privacy-preserving token transfers where balances and transfer amounts remain encrypted.

Terminology

  • Confidential Token: The ERC-7984 confidential token wrapper.

  • Underlying Token: The standard ERC-20 token wrapped by the confidential wrapper.

  • Wrapping: Converting ERC-20 tokens into confidential tokens.

  • Unwrapping: Converting confidential tokens back into ERC-20 tokens.

  • Rate: The conversion ratio between underlying token units and confidential token units (due to decimal differences).

  • Operator: An address authorized to transfer confidential tokens on behalf of another address.

  • Owner: The owner of the wrapper contract. In the FHEVM protocol, this is initially set to the Protocol DAO governance (see governance.md). Ownership will then be transferred to the underlying token's owner.

  • Registry: The registry contract that maps ERC-20 tokens to their corresponding confidential wrappers. More information here.

  • ACL: The Access Control List (ACL) contract that manages the permissions for encrypted amounts. More information in the FHEVM library documentation.

  • Input proof: A proof that the encrypted amount is valid. More information in the Zama SDK documentation.

  • Public decryption: A request to publicly decrypt an encrypted amount. More information in the Zama SDK documentation.

Contract information

Resource
Link

Deployed addresses

Structure

More information on the registry contract can be found in the wrapper-registry.md file.

Quick Start

Decimal conversion

Unsupported tokens

Get the confidential wrapper address of an ERC-20 token

An onchain registry contract maps ERC-20 tokens to their corresponding verified confidential wrappers. Make sure to check the registry contract to ensure the confidential wrapper is valid before wrapping. More information here.

Wrap ERC-20 → Confidential token

Important: Prior to wrapping, the confidential wrapper contract must be approved by the msg.sender on the underlying token.

The wrapper will mint the corresponding confidential token to the to address and refund the excess tokens to the msg.sender (due to decimal conversion).

It emits a Wrap event where roundedAmount is the actual amount of underlying tokens wrapped (i.e. amount rounded down to the nearest multiple of rate()):

Considerations:

  • amount must be a value using the same decimal precision as the underlying token.

  • to must not be the zero address.

Low amount handling

If the amount is less than the rate, the wrapping will succeed but the recipient will receive 0 confidential tokens and the excess tokens will be refunded to the msg.sender.

Unwrap confidential token → ERC-20

Unwrapping is a two-step asynchronous process: an unwrap must be first made and then finalized with finalizeUnwrap. The unwrap function can be called with or without an input proof.

1) Unwrap request

Unsupported from

With input proof

Input proof

To unwrap any amount of confidential tokens, the from address must first create an encrypted input to generate an encryptedAmount (externalEuint64) along its inputProof. The amount to be encrypted must use the same decimal precision as the confidential wrapper. More information in the Zama SDK documentation.

Alternatively, an unwrap request can be made without an input proof if the encrypted amount (euint64) is known to from. For example, this can be the confidential balance of from.

This requests an unwrap request of encryptedAmount confidential tokens from from. Considerations:

  • msg.sender must be from or an approved operator for from.

  • from must not be the zero address.

  • encryptedAmount will be burned in the request.

  • NO transfer of underlying tokens is made in this request.

It emits an UnwrapRequested event:

Without input proof

Alternatively, an unwrap request can be made without an input proof if the encrypted amount (euint64) is known to from. For example, this can be the confidential balance of from.

On top of the above unwrap request considerations:

  • msg.sender must be approved by ACL for the given encryptedAmount ⚠️ (see ACL documentation).

2) Finalize unwrap

The unwrap request must be finalized with finalizeUnwrap. The unwrapRequestId is the identifier emitted by the UnwrapRequested event in the unwrap function.

Public decryption

The encrypted amount unwrapAmount_ emitted by the UnwrapRequested event must be publicly decrypted to get the unwrapAmountCleartext along its decryptionProof. More information in the Zama SDK documentation.

This finalizes the unwrap request by sending the corresponding amount of underlying tokens to the to defined in the unwrap request.

It emits an UnwrapFinalized event:

Transfer confidential tokens

Transfer with input proof

Similarly to the unwrap process, transfers can be made with or without an input proof and the encrypted amount must be approved by the ACL for the msg.sender.

Direct transfer

Operator-based transfer

Considerations:

  • msg.sender must be from or an approved operator for from.

Transfer with callback

The callback can be used along an ERC-7984 receiver contract. After the transfer, the receiver's onTransferReceived callback is invoked. If the callback returns an encrypted false, the contract attempts to refund the transferred amount back to the sender.

The returned transferred ciphertext is a fresh net amount computed from the transfer and any refund attempt. It is only granted as a transient FHE allowance to msg.sender for the current transaction, so callers that need to keep using that value later must obtain their own persistent allowance.

Best-effort refund

Operator-based transfer with callback

The callback can be used along an ERC-7984 receiver contract.

The returned transferred ciphertext is also transient-only here: msg.sender can use it during the current transaction, but no persistent allowance is granted on that net amount.

Considerations:

  • msg.sender must be from or an approved operator for from.

Check the conversion rate and decimals

Examples:

Underlying Decimals
Wrapper Decimals
Rate
Effect

18

6

10^12

1 wrapped = 10^12 underlying

6

6

1

1:1 mapping

2

2

1

1:1 mapping

Check supplies

Non-confidential total supply

The wrapper exposes a non-confidential view of the total supply, computed from the underlying ERC20 balance held by the wrapper contract. This value may be higher than confidentialTotalSupply() if tokens are sent directly to the wrapper outside of the wrapping process.

Total Value Shielded (TVS)

This view function is useful for getting a good approximation of the wrapper's Total Value Shielded (TVS).

Encrypted (confidential) total supply

The actual supply tracked by the confidential token contract, represented as an encrypted value. To determine the cleartext value, you need to request decryption and appropriate ACL authorization.

Maximum total supply

The maximum number of wrapped tokens supported by the encrypted datatype (uint64 limit). If this maximum is exceeded, wrapping new tokens will revert.

Integration patterns

Operator system

Delegate transfer capabilities with time-based expiration:

Query ongoing unwrap request details

Amount disclosure

Optionally reveal encrypted amounts publicly:

Check ACL permissions

Before using encrypted amounts in transactions, callers must be authorized:

Transfer functions with euint64 (not externalEuint64) require the caller to already have ACL permission for that ciphertext. More information in the FHEVM library documentation.

Architecture

Events

Event
Description

Wrap(to, roundedAmount, encryptedWrappedAmount)

Emitted when tokens are wrapped

ConfidentialTransfer(from, to, encryptedAmount)

Emitted on every transfer (including mint/burn)

OperatorSet(holder, operator, until)

Emitted when operator permissions change

UnwrapRequested(receiver, unwrapRequestId, encryptedAmount)

Emitted when unwrap is initiated

UnwrapFinalized(receiver, unwrapRequestId, encryptedAmount, cleartextAmount)

Emitted when unwrap completes

AmountDiscloseRequested(encryptedAmount, requester)

Emitted when disclosure is requested

AmountDisclosed(encryptedAmount, cleartextAmount)

Emitted when amount is publicly disclosed

Errors

Error
Cause

ERC7984InvalidReceiver(receiver)

Transfer to zero address

ERC7984InvalidSender(sender)

Transfer from zero address

ERC7984UnauthorizedSpender(holder, spender)

Caller not authorized as operator

ERC7984ZeroBalance(holder)

Sender has never held tokens

ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)

Caller lacks ACL permission for ciphertext

ERC7984UnauthorizedCaller(caller)

Invalid caller for operation

InvalidUnwrapRequest(unwrapRequestId)

Finalizing non-existent unwrap request

ERC7984TotalSupplyOverflow()

Minting would exceed uint64 max

Important Considerations

Ciphertext uniqueness assumption

The unwrap mechanism stores requests in a mapping keyed by ciphertext and the current implementation assumes these ciphertexts are unique. This holds in this very specific case but be aware of this architectural decision as it is NOT true in the general case.

Maximum number of decimals

The maximum number of decimals _maxDecimals() for the confidential token is currently set to 6 decimals only. This is due to FHE limitations as confidential balances must be represented by the euint64 encrypted datatype.

It is possible that future implementations of the wrapper set a higher _maxDecimals() value to better suit the needs of the underlying token. For example, cWBTC might require 8 decimals since using only 6 would make the smallest unit impractically expensive.

At deployment, the confidential wrapper sets its number of decimals as:

  • the number of decimals of the underlying token if it is less than _maxDecimals()

  • _maxDecimals() otherwise

Example with _maxDecimals() set to 6

Underlying Decimals
Wrapper Decimals
Example

18

6

ZAMA/cZAMA

6

6

USDT/cUSDT

2

2

GUSD/cGUSD

Once a confidential wrapper contract is deployed, this number cannot be updated. It can be viewed with the following view function:

Maximum total supply

The maximum total supply for the confidential token is currently set to type(uint64).max (2^64 - 1) due to FHE limitations.

Non-standard token types

The wrapper assumes the full transfer amount is received when minting. Tokens that deviate from this assumption, or whose supply changes independently of wrap/unwrap operations, are not supported and may result in undercollateralization or loss of yield.

Type
Behavior
Example
Wrapper impact

Fee-on-transfer

A fee is deducted from the transferred amount

SafeMoon, PAXG

The wrapper mints more shares than the underlying balance it receives, leading to undercollateralization

Deflationary

Token supply decreases over time via burns on transfer or scheduled reductions

BOMB

Equivalent to fee-on-transfer; the same undercollateralization risk applies

Inflationary

New tokens are minted over time to addresses other than existing holders

Governance tokens with scheduled emissions

Supported: The wrapper is not directly impacted, but holders of the confidential token are subject to the same dilution as holders of the underlying token

Rebasing (up)

Holder balances increase automatically over time to distribute yield

aUSDC, stETH

Yield accrues to the wrapper contract rather than to individual holders; wrapped positions do not earn rewards

Rebasing (down)

Holder balances decrease automatically, for example due to slashing

stETH (slashing)

The wrapper holds fewer underlying tokens than shares outstanding, resulting in undercollateralization

Pausable

A privileged account can suspend all token transfers

USDC, USDT

Wrap and unwrap operations revert for the duration of the pause

Blocklist/allowlist

A privileged account can restrict transfers to or from specific addresses

USDC, USDT

The wrapper contract address may be blocked, preventing all wrap and unwrap operations

Upgradeable

The token implementation can be replaced after deployment

USDC (proxy)

A logic upgrade may alter token behavior in ways that are incompatible with the wrapper

Multiple entry points

Two contract addresses share the same underlying balance

Old Synthetix SNX/ProxyERC20

The same underlying balance can be wrapped twice, inflating the confidential supply

Flash-mintable

Tokens can be minted without collateral within a single transaction

DAI (flash mint)

Transient supply spikes may interfere with inferredTotalSupply() based checks

ERC-777 hooks

Transfers invoke callbacks on the sender and receiver

imBTC

Callbacks introduce reentrancy vectors during wrap and unwrap operations

Non-standard decimals

The token uses fewer than 18 decimals

USDC (6), WBTC (8), GUSD (2)

Supported: The wrapper normalizes precision automatically via rate() -- See the section on decimal conversion

Interface Support (ERC-165)

Interface
Interface ID

IERC7984

0x4958f2a4

IERC7984ERC20Wrapper

0x1f1c62b2

IERC1363Receiver

0x88a7ca5c

IERC165

0x01ffc9a7

Upgradeability

The contract uses UUPS (Universal Upgradeable Proxy Standard) with 2-step ownership transfer. Only the owner can upgrade the contract. Initially, the owner is set to the Protocol DAO governance (see governance.md). Ownership will then be transferred to the underlying token's owner.

Last updated