08 — Encrypted Token
Combine the ERC-20 pattern from chapter 06 with encrypted<u256> balances so no on-chain observer can read balances or transfer amounts.
contract EncryptedToken {
field name: String = "PrivaCoin"
field symbol: String = "PVC"
field decimals: u8 = 18
// Balances are FHE ciphertexts — observers see only random-looking bytes
field balances: Map<Address, encrypted<u256>>
// Total supply is public for transparency
field total_supply: u256
event EncryptedTransfer(from: indexed Address, to: indexed Address)
error InsufficientEncryptedBalance
init(initial_supply: u256) {
self.total_supply = initial_supply;
self.balances[msg.sender] = fhe_encrypt(initial_supply, fhe_owner_key());
}
// Transfer: caller provides encrypted amount; contract checks ciphertext > balance
action private_transfer(to: Address, enc_amount: encrypted<u256>) {
let sender_bal = self.balances[msg.sender];
// fhe_lt returns encrypted<Bool> — no plaintext comparison
let is_sufficient = fhe_lt(enc_amount, sender_bal);
// require works on encrypted<Bool> — reverts if ciphertext decrypts to false
require(is_sufficient, InsufficientEncryptedBalance);
self.balances[msg.sender] = fhe_sub(sender_bal, enc_amount);
self.balances[to] = fhe_add(self.balances[to], enc_amount);
emit EncryptedTransfer(from: msg.sender, to: to);
}
// Let the owner decrypt a specific balance (for auditing)
action audit_balance(addr: Address) -> u256 {
only(owner);
return fhe_decrypt(self.balances[addr], fhe_owner_key());
}
}
How the viewer key works
The fhe_owner_key() built-in resolves to the public key stored in the contract’s deployment metadata. Only the holder of the corresponding private key can decrypt. This is analogous to the Viewer Pass mechanism on Aster Chain — selective disclosure without exposing data to the validator set.
Client-side encryption
Off-chain code that calls private_transfer must encrypt the amount before submitting:
import { CovenantClient, fheEncrypt } from "@covenant-lang/sdk";
const client = new CovenantClient(provider);
const pubkey = await client.fhePubkey(contractAddress);
const encAmt = await fheEncrypt(1000n, pubkey); // Uint8Array ciphertext
await contract.private_transfer(recipient, encAmt);
Limitations
- FHE balances cannot be read by block explorers without the owner key.
fhe_ltwith 256-bit integers costs ~200 M gas on standard EVM; use Aster Chain or a dedicated L2 with native FHE precompiles for production.- ERC-20
allowance/transferFrompattern requires an encrypted allowance map — left as an exercise (see theexamples/encrypted-allowancesrepo).