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 18 decimals, this is equivalent to about 18.4 million tokens with full precision.
The fundamental transfer operation moves encrypted tokens from the caller to a recipient:
Copy
Ask AI
function confidentialTransfer( address to, InEuint64 memory inValue) external returns (euint64 transferred) { // Convert encrypted input to euint64 euint64 value = FHE.asEuint64(inValue); // 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.
The _update function is the core of all balance changes:
Copy
Ask AI
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 _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); _encTotalSupply = FHE.add(_encTotalSupply, transferred); } if (to == address(0)) { // Burning - update total supply _indicatedTotalSupply = _decrementIndicator(_indicatedTotalSupply); _encTotalSupply = FHE.sub(_encTotalSupply, 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 decrypt the transferred amount FHE.allow(transferred, msg.sender); // Hide totalSupply FHE.allowThis(_encTotalSupply); // 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:
Copy
Ask AI
// 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(_encTotalSupply);
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 ERC20InvalidReceiver(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:
Copy
Ask AI
function _confidentialMint(address account, euint64 value) internal returns (euint64 transferred){ if (account == address(0)) { revert ERC20InvalidReceiver(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 ERC20InvalidSender(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 emits standard ERC20 events with indicator values:
Copy
Ask AI
// Standard ERC20 Transfer eventevent Transfer(address indexed from, address indexed to, uint256 value);// The value is always indicatorTick, regardless of actual transfer amountemit Transfer(from, to, indicatorTick());
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:
Copy
Ask AI
// 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()