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.
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)// 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 whenNotPausedSilent 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
| Function | Access | Description |
|---|---|---|
setTreasury(address) | Owner | Update fee treasury address |
treasuryWithdraw() | Treasury or Owner | Withdraw accumulated USDC fees |
pause() | Owner | Emergency pause all operations |
unpause() | Owner | Resume operations |
transferOwnership(address) | Owner | Start 2-step ownership transfer |
acceptOwnership() | Pending owner | Complete 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
| Constant | Value | Description |
|---|---|---|
FEE_BPS | 10 | 0.1% fee rate |
BPS | 10,000 | Basis point denominator |
MIN_PROTOCOL_FEE | 10,000 | 0.01 USDC minimum fee |
MAX_FEE_BPS | 100 | 1% governance safety limit |