FHERC20’s core functionality revolves around maintaining complete confidentiality for token balances and transfers while still enabling all the operations users expect from a token. This page explores the fundamental features that make FHERC20 work.
The euint64 type can store values up to 2^64 - 1, which is 18,446,744,073,709,551,615. For tokens with 6 decimals (recommended), this is equivalent to about 18.4 trillion tokens with full precision.
The fundamental transfer operation moves encrypted tokens from the caller to a recipient:
function confidentialTransfer( address to, InEuint64 memory encryptedAmount) external returns (euint64 transferred) { // Convert encrypted input to euint64 euint64 value = FHE.asEuint64(encryptedAmount); // Perform encrypted transfer return _transfer(msg.sender, to, value);}
Use the InEuint64 overload when accepting user input from off-chain. Use the euint64 overload for contract-to-contract transfers where the value is already encrypted. Note: the euint64 overload requires the caller to be authorized via the FHE ACL for the given amount.
The _update function is the core of all balance changes. It uses FHESafeMath for overflow/underflow protection on encrypted values:
function _update( address from, address to, euint64 value) internal virtual returns (euint64 transferred) { // Handle transfers (not mints or burns) if (from != address(0)) { // Check if user has sufficient balance // If not, transfer zero instead (privacy-preserving) transferred = FHE.select( value.lte(_confidentialBalances[from]), value, FHE.asEuint64(0) ); // Subtract from sender using safe math _confidentialBalances[from] = FHE.sub(_confidentialBalances[from], transferred); _indicatedBalances[from] = _decrementIndicator(_indicatedBalances[from]); } else { // Minting transferred = value; } if (from == address(0)) { // Minting - update total supply _indicatedTotalSupply = _incrementIndicator(_indicatedTotalSupply); _confidentialTotalSupply = FHE.add(_confidentialTotalSupply, transferred); } if (to == address(0)) { // Burning - update total supply _indicatedTotalSupply = _decrementIndicator(_indicatedTotalSupply); _confidentialTotalSupply = FHE.sub(_confidentialTotalSupply, transferred); } else { // Normal transfer - add to recipient _confidentialBalances[to] = FHE.add(_confidentialBalances[to], transferred); _indicatedBalances[to] = _incrementIndicator(_indicatedBalances[to]); } // Update CoFHE Access Control List (ACL) if (euint64.unwrap(_confidentialBalances[from]) != 0) { FHE.allowThis(_confidentialBalances[from]); FHE.allow(_confidentialBalances[from], from); FHE.allow(transferred, from); } if (euint64.unwrap(_confidentialBalances[to]) != 0) { FHE.allowThis(_confidentialBalances[to]); FHE.allow(_confidentialBalances[to], to); FHE.allow(transferred, to); } // Allow the caller to access the transferred amount FHE.allow(transferred, msg.sender); // Hide totalSupply FHE.allowThis(_confidentialTotalSupply); // Emit events emit Transfer(from, to, _indicatorTick); emit ConfidentialTransfer(from, to, euint64.unwrap(transferred)); return transferred;}
Zero-Replacement Behavior: If a user attempts to transfer more than their balance, FHERC20 does not revert. Instead, it transfers zero tokens. This preserves privacy by not revealing whether the user had sufficient balance.
As shown in the _update function above, access control is managed inline as part of each balance update:
// Update CoFHE Access Control List (ACL)if (euint64.unwrap(_confidentialBalances[from]) != 0) { FHE.allowThis(_confidentialBalances[from]); // Contract can use balance FHE.allow(_confidentialBalances[from], from); // User can query balance FHE.allow(transferred, from); // User can see transferred amount}if (euint64.unwrap(_confidentialBalances[to]) != 0) { FHE.allowThis(_confidentialBalances[to]); FHE.allow(_confidentialBalances[to], to); FHE.allow(transferred, to);}// Allow the caller to decrypt the transferred amountFHE.allow(transferred, msg.sender);// Hide totalSupply (only contract has access)FHE.allowThis(_confidentialTotalSupply);
This ensures:
✅ Users can access their own balances
✅ The contract can perform operations on balances
✅ Transfer participants (sender, receiver, and caller) can see the transferred amount
✅ Total supply is only accessible by the contract
Learn more about FHE access control in the Access Control guide.
function _mint(address account, uint64 value) internal returns (euint64 transferred){ if (account == address(0)) { revert FHERC20InvalidReceiver(address(0)); } // Convert plaintext value to encrypted and mint // The _update function handles total supply updates when from == address(0) transferred = _update(address(0), account, FHE.asEuint64(value));}
There’s also a confidential mint variant that accepts already-encrypted values:
function _confidentialMint(address account, euint64 value) internal returns (euint64 transferred){ if (account == address(0)) { revert FHERC20InvalidReceiver(address(0)); } // Value is already encrypted transferred = _update(address(0), account, value);}
function _burn(address account, uint64 value) internal returns (euint64 transferred){ if (account == address(0)) { revert FHERC20InvalidSender(address(0)); } // The _update function handles total supply updates when to == address(0) transferred = _update(account, address(0), FHE.asEuint64(value));}
Like transfers, burning uses the zero-replacement pattern. If you attempt to burn more than an account’s balance, zero tokens are burned instead of reverting.
FHERC20 includes optional disclosure functions for transparency when needed. Accounts with access to an encrypted amount can voluntarily reveal it on-chain.
/// @dev The given receiver is invalid for transfers.error FHERC20InvalidReceiver(address receiver);/// @dev The given sender is invalid for transfers.error FHERC20InvalidSender(address sender);/// @dev The holder is not authorized to spend on behalf of spender.error FHERC20UnauthorizedSpender(address holder, address spender);/// @dev The holder is trying to send tokens but has a balance of 0.error FHERC20ZeroBalance(address holder);/// @dev The caller does not have access to the encrypted amount.error FHERC20UnauthorizedUseOfEncryptedAmount(euint64 amount, address user);/// @dev The caller is not authorized for the current operation.error FHERC20UnauthorizedCaller(address caller);/// @dev Reverts when a cleartext ERC-20 function is called on a confidential token.error FHERC20IncompatibleFunction();
FHERC20 emits standard ERC20 events with indicator values, plus confidential-specific events:
// Standard ERC20 Transfer event (value is always indicatorTick)event Transfer(address indexed from, address indexed to, uint256 value);emit Transfer(from, to, indicatorTick());// Confidential transfer event with encrypted amount handleevent ConfidentialTransfer(address indexed from, address indexed to, euint64 indexed amount);// Amount disclosure eventevent AmountDisclosed(euint64 indexed encryptedAmount, uint64 amount);// Operator permission eventevent OperatorSet(address indexed holder, address indexed operator, uint48 until);
The Transfer event doesn’t reveal the actual transfer amount—only that a transfer occurred. The value field always contains indicatorTick to maintain ERC20 compatibility while preserving privacy.
For privacy reasons, several standard ERC20 functions intentionally revert:
// These functions are not supportedfunction transfer(address, uint256) public pure returns (bool) { revert FHERC20IncompatibleFunction();}function allowance(address, address) external pure returns (uint256) { revert FHERC20IncompatibleFunction();}function approve(address, uint256) external pure returns (bool) { revert FHERC20IncompatibleFunction();}function transferFrom(address, address, uint256) public pure returns (bool) { revert FHERC20IncompatibleFunction();}
Instead, use:
confidentialTransfer() instead of transfer()
setOperator() instead of approve()
confidentialTransferFrom() instead of transferFrom()