ERC-7984 Confidential Token

ConfidentialUSDC wraps USDC into FHE-encrypted cUSDC for private transfers.

Overview

ConfidentialUSDC is an ERC-7984 token built on OpenZeppelin Confidential Contracts. It wraps plaintext USDC into encrypted cUSDC using Zama's fhEVM. All balances are stored as euint64 — FHE-encrypted 64-bit unsigned integers.

Inherits from: ERC7984, ERC7984ERC20Wrapper, Ownable2Step, Pausable, ReentrancyGuard

Wrapping USDC

Convert plaintext USDC into encrypted cUSDC. A protocol fee is deducted before minting.

ConfidentialUSDC.solsolidity
function wrap(address to, uint256 amount) external nonReentrant whenNotPaused
// 1. Validates amount > 0
// 2. Calculates fee: max(amount * 10 / 10000, 10000)  →  max(0.1%, 0.01 USDC)
// 3. Transfers full USDC from sender to contract
// 4. Mints (amount - fee) as encrypted cUSDC to 'to'
// 5. Emits: ConfidentialTransfer(address(0), to, encryptedAmount)
example.tstypescript
// Approve + wrap 100 USDC
await usdc.approve(cusdcAddress, 100_000_000n);
await cusdc.wrap(walletAddress, 100_000_000n);
// Result: 99,900,000 cUSDC minted (0.1% = 100,000 fee)

Confidential Transfers

Transfer encrypted cUSDC peer-to-peer with zero protocol fee. The transfer amount is FHE-encrypted — no on-chain observer can see the value.

// Transfer with new encryption (client encrypts amount)
function confidentialTransfer(
  address to,
  externalEuint64 encryptedAmount,
  bytes calldata inputProof
) external whenNotPaused

// Transfer with existing handle (contract-to-contract)
function confidentialTransfer(
  address to,
  euint64 amount
) external whenNotPaused

// Transfer from (operator)
function confidentialTransferFrom(
  address from,
  address to,
  externalEuint64 encryptedAmount,
  bytes calldata inputProof
) external whenNotPaused

Silent Failure Pattern

Silent Failure Pattern

In traditional ERC-20 tokens, a transfer with insufficient balance reverts with an error. But in FHE, the balance is encrypted — the contract cannot evaluate an encrypted boolean to decide whether to revert. A require(balance >= amount) check is impossible because both values are ciphertexts. If the contract reverted only when the balance was too low, an observer could deduce balance information from which transactions revert vs. succeed.

Instead, MARC uses FHE.select(hasEnough, amount, zero): the contract always succeeds, but silently transfers 0 when the balance is insufficient. This preserves privacy at the cost of bounded risk — a server may give one free API response before detecting the zero-value transfer via the SDK's SilentFailureGuard.

This means servers bear a bounded risk: one free API response per failed payment. The SDK includes a SilentFailureGuard to mitigate this by tracking suspicious patterns.

Unwrapping to USDC

Unwrapping is a 2-step async process because the encrypted amount must be decrypted by the KMS.

// Step 1: Burn encrypted tokens, request KMS decryption
function unwrap(
  address from,
  address to,
  externalEuint64 encryptedAmount,
  bytes calldata inputProof
) external
// Emits: UnwrapRequested(to, burntAmount)

// Step 2: Finalize with KMS decryption proof
function finalizeUnwrap(
  euint64 burntAmount,
  uint64 burntAmountCleartext,
  bytes calldata decryptionProof
) external nonReentrant whenNotPaused
// Emits: UnwrapFinalized(to, burntAmount, clearAmount)

The KMS calls finalizeUnwrap with the decrypted amount and a cryptographic proof. The contract verifies the proof, deducts the fee, and sends plaintext USDC to the recipient.

Operators

Operators can transfer cUSDC on behalf of the token holder. Approvals include an expiry timestamp.

// Approve operator with expiry
function setOperator(address operator, uint48 until) external whenNotPaused
// Emits: OperatorSet(holder, operator, until)

// Check approval
function isOperator(address holder, address spender) external view returns (bool)

Admin Functions

FunctionAccessDescription
setTreasury(address)OwnerUpdate fee treasury address
treasuryWithdraw()Treasury or OwnerWithdraw accumulated USDC fees
pause()OwnerEmergency pause all operations
unpause()OwnerResume operations
transferOwnership(address)OwnerStart 2-step ownership transfer
acceptOwnership()Pending ownerComplete ownership transfer

Events

event ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount);
event OperatorSet(address indexed holder, address indexed operator, uint48 until);
event UnwrapRequested(address indexed receiver, bytes32 amount);
event UnwrapFinalized(address indexed receiver, bytes32 encryptedAmount, uint64 cleartextAmount);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
event TreasuryWithdrawn(address indexed treasury, uint256 amount);

Constants

ConstantValueDescription
FEE_BPS100.1% fee rate
BPS10,000Basis point denominator
MIN_PROTOCOL_FEE10,0000.01 USDC minimum fee
MAX_FEE_BPS1001% governance safety limit