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
| Property | Value |
|---|---|
| License | MIT |
| Solidity | ^0.8.25 |
| Inheritance | ERC20, 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:
| Name | Type | Description |
|---|---|---|
| to | address | Recipient address |
| value | euint128 | Encrypted amount |
Returns:
| Name | Type | Description |
|---|---|---|
| transferred | euint128 | Actual 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:
| Name | Type | Description |
|---|---|---|
| from | address | Sender address |
| to | address | Recipient address |
| value | euint128 | Encrypted 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:
| Name | Type | Description |
|---|---|---|
| amount | uint256 | Plaintext amount to wrap |
Effects:
- Burns plaintext tokens from caller
- Adds equivalent encrypted tokens to caller's balance
- 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:
- Deducts from encrypted balance
- Initiates async decryption
- 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:
- Converts
InEuint128toeuint128 - 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);