Skip to main content

FHERC20

ERC20 token standard extended with encrypted balance support.

Overview

FHERC20 (Fully Homomorphic Encryption ERC20) extends the standard ERC20 interface to support:

  • Encrypted balances stored as euint128
  • Encrypted transfers between addresses
  • Permission-based decryption
  • Conversion between plaintext and encrypted balances

Interface

IFHERC20

interface IFHERC20 {
function encBalanceOf(address account) external view returns (euint128);

function encTransfer(
address to,
euint128 value
) external returns (euint128 transferred);

function encTransferFrom(
address from,
address to,
euint128 value
) external returns (euint128 transferred);

function encTotalSupply() external view returns (euint128);

function isFherc20() external view returns (bool);

function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}

TestFHERC20 Implementation

Lunarys includes a test implementation for development and testing.

Contract Details

PropertyValue
LicenseMIT
Solidity^0.8.25
InheritanceERC20, Ownable, IFHERC20

State Variables

mapping(address => euint128) private _encBalances;  // Encrypted balances
euint128 private _encTotalSupply; // Encrypted total supply

Functions

Balance Queries

encBalanceOf

Get the encrypted balance of an account.

function encBalanceOf(address account) public view returns (euint128)

Returns: Encrypted balance as euint128.

encTotalSupply

Get the encrypted total supply.

function encTotalSupply() public view returns (euint128)

Encrypted Transfers

encTransfer

Transfer encrypted tokens to a recipient.

function encTransfer(
address to,
euint128 value
) public returns (euint128 transferred)

Parameters:

NameTypeDescription
toaddressRecipient address
valueeuint128Encrypted amount

Returns:

NameTypeDescription
transferredeuint128Actual amount transferred (0 if insufficient)

Behavior:

  • If sender has sufficient balance: transfers full amount
  • If sender has insufficient balance: transfers 0 (no revert)
  • Grants permissions to both sender and recipient

encTransferFrom

Transfer encrypted tokens from one address to another.

function encTransferFrom(
address from,
address to,
euint128 value
) public returns (euint128 transferred)

Parameters:

NameTypeDescription
fromaddressSender address
toaddressRecipient address
valueeuint128Encrypted amount

Note: In production, this requires prior approval or permit.

Wrap/Unwrap

wrap

Convert plaintext tokens to encrypted tokens.

function wrap(uint256 amount) external

Parameters:

NameTypeDescription
amountuint256Plaintext amount to wrap

Effects:

  1. Burns plaintext tokens from caller
  2. Adds equivalent encrypted tokens to caller's balance
  3. Grants permissions to caller

wrapEncrypted

Wrap with an already-encrypted input.

function wrapEncrypted(InEuint128 memory amountIn) external

requestUnwrap

Request to convert encrypted tokens back to plaintext.

function requestUnwrap(euint128 amount) external

Effects:

  1. Deducts from encrypted balance
  2. Initiates async decryption
  3. Plaintext tokens can be claimed after decryption completes

Testing Helpers

mint

Mint plaintext tokens (owner only).

function mint(address to, uint256 amount) external onlyOwner

mintEncrypted

Mint encrypted tokens directly (owner only).

function mintEncrypted(address to, uint256 amount) external onlyOwner

requestBalanceDecryption

Request decryption of caller's encrypted balance.

function requestBalanceDecryption() external

getDecryptedBalance

Get decrypted balance after requesting decryption.

function getDecryptedBalance(address account) external view returns (
uint256 balance,
bool ready
)

Transfer with Encrypted Input

encTransferIn

Transfer using client-side encrypted input.

function encTransferIn(
address to,
InEuint128 memory valueIn
) external returns (euint128 transferred)

This function:

  1. Converts InEuint128 to euint128
  2. Calls standard encTransfer

Events

event EncryptedTransfer(
address indexed from,
address indexed to,
uint256 amountHandle
);

event Wrapped(address indexed account, uint256 amount);

event Unwrapped(address indexed account, uint256 amountHandle);

Errors

error InsufficientEncryptedBalance();
error ZeroAddress();

FHE Permission Management

The implementation carefully manages FHE permissions:

// After updating balances
FHE.allowThis(_encBalances[from]); // Contract can use value
FHE.allowThis(_encBalances[to]); // Contract can use value
FHE.allow(_encBalances[from], from); // Sender can decrypt
FHE.allow(_encBalances[to], to); // Recipient can decrypt
FHE.allowThis(transferred); // Contract can use return value
FHE.allow(transferred, from); // Sender knows result
FHE.allow(transferred, to); // Recipient knows result

Insufficient Balance Handling

Unlike standard ERC20 which reverts on insufficient balance, FHERC20 returns 0:

ebool hasEnough = fromBalance.gte(value);
transferred = FHE.select(hasEnough, value, FHE.asEuint128(0));

This pattern:

  • Maintains privacy (no revert reveals balance info)
  • Allows calling code to check the result
  • Works with FHE's conditional operations

Self-Transfer Optimization

Self-transfers are optimized to avoid unnecessary balance updates:

if (from == to) {
FHE.allowThis(transferred);
FHE.allow(transferred, from);
emit EncryptedTransfer(from, to, euint128.unwrap(transferred));
return transferred;
}

Usage Example

// Deploy token
TestFHERC20 token = new TestFHERC20("Test Token", "TEST", owner);

// Mint plaintext tokens
token.mint(user, 1000 ether);

// Wrap to encrypted
token.wrap(100 ether);

// Check encrypted balance handle
euint128 balance = token.encBalanceOf(user);

// Transfer encrypted
token.encTransfer(recipient, encryptedAmount);

// Or use client-encrypted input
token.encTransferIn(recipient, clientEncryptedInput);

// Unwrap back to plaintext
token.requestUnwrap(encryptedAmount);
// Wait for decryption...
// Plaintext tokens are now available

Integration with PrivacyPool

The pool uses FHERC20 tokens through the interface:

// Pull tokens into pool
euint128 transferred = token.encTransferFrom(
msg.sender,
address(this),
amount128
);

// Send tokens to user
token.encTransfer(to, amount128);

The pool converts between euint64 (internal) and euint128 (FHERC20):

euint128 amount128 = FHE.asEuint128(amount64);
euint64 result64 = FHE.asEuint64(result128);