Skip to main content

Overview

In FHEVM, all data is encrypted on-chain. The Access Control List (ACL) system determines which addresses can operate on encrypted values. Every ciphertext has an associated ACL that tracks authorized addresses.
Key insight: Without proper ACL management, even the contract that created a ciphertext cannot use it in future transactions.

Why ACL is Necessary

The Problem

Consider a token contract storing encrypted balances:
mapping(address => euint64) private _balances;
Without ACL:
  • The contract itself cannot read stored ciphertexts in future transactions
  • Users cannot decrypt their own balances
  • Other contracts cannot interact with the ciphertexts

The Solution

The ACL system provides granular, per-ciphertext permissions. Each time you create or update a ciphertext, you must explicitly grant access.

Core ACL Functions

FHE.allowThis(handle) — Self-Permission

Grants the current contract permission to use a ciphertext.
ACLDemo.sol
euint32 private _ownerSecret;

function setSecret(uint32 value) external onlyOwner {
    _ownerSecret = FHE.asEuint32(value);
    FHE.allowThis(_ownerSecret);  // Contract can use _ownerSecret later
    FHE.allow(_ownerSecret, msg.sender);
}
Critical: Every FHE operation produces a new ciphertext with a new handle. You must call FHE.allowThis() after every state variable assignment.
ACLDemo.sol
function incrementSecret() external onlyOwner {
    _ownerSecret = FHE.add(_ownerSecret, 1);
    // _ownerSecret is now a NEW ciphertext — must re-grant access
    FHE.allowThis(_ownerSecret);
    FHE.allow(_ownerSecret, msg.sender);
}

FHE.allow(handle, address) — Grant to Specific Address

Grants persistent access to a specific address (EOA or contract).
MultiUserVault.sol
function deposit(externalEuint64 encAmount, bytes calldata inputProof) external {
    euint64 amount = FHE.fromExternal(encAmount, inputProof);
    _deposits[msg.sender] = FHE.add(_deposits[msg.sender], amount);
    FHE.allowThis(_deposits[msg.sender]);      // Contract access
    FHE.allow(_deposits[msg.sender], msg.sender);   // User access
}
Always grant both FHE.allowThis() and FHE.allow(handle, user) for user-owned data:
  • allowThis — contract can use it in future operations
  • allow(user) — user can decrypt it client-side

FHE.allowTransient(handle, address) — Temporary Permission

Grants access that lasts only for the current transaction. Useful for inter-contract calls.
ACLDemo.sol
function grantTransientAccess(address to) external onlyOwner {
    FHE.allowTransient(_ownerSecret, to);
    // 'to' can use _ownerSecret only within this transaction
}
FeatureFHE.allow()FHE.allowTransient()
DurationPermanentTransaction only
Storage costHigherLower
Use caseUser accessInter-contract calls

FHE.makePubliclyDecryptable(handle) — Public Reveal

Makes an encrypted value decryptable by anyone. Use for vote results, auction winners, etc.
ACLDemo.sol
function makePublic() external onlyOwner {
    FHE.makePubliclyDecryptable(_ownerSecret);
}
Irreversible: Once made publicly decryptable, ANYONE can see the plaintext. Use only for values meant to be public.

FHE.isSenderAllowed(handle) — Permission Check

Returns true if msg.sender has access to a ciphertext.
ACLDemo.sol
function getSecret() external view returns (euint32) {
    require(FHE.isSenderAllowed(_ownerSecret), "Not authorized to access secret");
    return _ownerSecret;
}

Common Patterns

Pattern 1: Multi-User Vault with ACL Isolation

MultiUserVault.sol
mapping(address => euint64) private _deposits;

function withdraw(externalEuint64 encAmount, bytes calldata inputProof) external {
    euint64 amount = FHE.fromExternal(encAmount, inputProof);
    ebool hasEnough = FHE.ge(_deposits[msg.sender], amount);
    euint64 withdrawAmount = FHE.select(hasEnough, amount, FHE.asEuint64(0));
    _deposits[msg.sender] = FHE.sub(_deposits[msg.sender], withdrawAmount);
    
    // Reset ACL after operation
    FHE.allowThis(_deposits[msg.sender]);
    FHE.allow(_deposits[msg.sender], msg.sender);
}

Pattern 2: Secret Sharing

UserDecrypt.sol
function shareSecret(address to) external {
    require(euint32.unwrap(_userSecrets[msg.sender]) != 0, "No secret stored");
    FHE.allow(_userSecrets[msg.sender], to);
    // Now 'to' can decrypt msg.sender's secret
}

Critical ACL Rules

New Handle = New ACL

Every FHE operation creates a new ciphertext with an empty ACL.

No Automatic Access

The creator gets no automatic access — always call FHE.allowThis().

Per-Handle, Not Per-Variable

ACL is tied to the handle, not the storage slot.

No Direct Revocation

Create a new ciphertext to “revoke” — old handle retains access.

Common Mistakes

Mistake 1: Forgetting FHE.allowThis() After Update

// ❌ WRONG
function update() public {
    _value = FHE.add(_value, FHE.asEuint32(1));
    // Next transaction using _value WILL FAIL
}

// ✅ CORRECT
function update() public {
    _value = FHE.add(_value, FHE.asEuint32(1));
    FHE.allowThis(_value);
}

Mistake 2: Not Granting User Access

// ❌ WRONG
function setBalance(address user, uint64 amount) public {
    _balances[user] = FHE.asEuint64(amount);
    FHE.allowThis(_balances[user]);
    // User cannot decrypt their own balance!
}

// ✅ CORRECT
function setBalance(address user, uint64 amount) public {
    _balances[user] = FHE.asEuint64(amount);
    FHE.allowThis(_balances[user]);
    FHE.allow(_balances[user], user);
}

Summary

FunctionPurposeDuration
FHE.allowThis(handle)Grant current contract accessPersistent
FHE.allow(handle, addr)Grant specific address accessPersistent
FHE.allowTransient(handle, addr)Grant temporary accessTransaction only
FHE.makePubliclyDecryptable(handle)Reveal to everyonePersistent (irreversible)
FHE.isSenderAllowed(handle)Check if msg.sender has accessN/A (view)

Next Module

Learn how users submit truly private data using client-side encryption

Build docs developers (and LLMs) love