Ethereum devs are no strangers to public-by-default data. It’s one of the foundational features of blockchain: every transaction, every variable, every storage slot—transparent, inspectable, immutable.
But what happens when you want to build applications that don’t expose user data to the entire world? Enter Fully Homomorphic Encryption (FHE) and the CoFHE coprocessor. For the first time, Ethereum contracts can compute on encrypted values… but with this power comes a new responsibility:
Controlling who’s allowed to use or view encrypted data.
That’s where access control and permits come in.
🤔 Why does access control even matter in FHE?
Let’s say a user encrypts a value (like a secret bid or their balance) and sends it to your contract. You store that encrypted value as an euint32
. Everything looks great!
Now imagine someone else sees that encrypted handle on-chain and tries to use it:
function attack() public {
FHE.decrypt(seenHandle); // 👀 tries to decrypt someone else’s data
}
If there were no protections, this would totally defeat the point of encryption. That’s why every encrypted handle in CoFHE is tied to an access control list (ACL). It enforces strict rules around who is allowed to operate on or decrypt a given ciphertext.
By default:
- The contract that creates a value (via
FHE.asEuint32()
) gets temporary access for that transaction only. - No one else can access that ciphertext unless explicitly granted.
🧠 The mental model: ownership, scope, and sealing
When working with encrypted values in Solidity using FHE.sol
, think in terms of who has permission to do what, and for how long.
There are two types of access:
-
Contract-level access — via
FHE.allowThis()
orFHE.allow()
-
User-level access — via
FHE.allow(ciphertext, address)
+ sealed output + permit incofhejs
There are also two time scopes:
- Transient: access only lasts during the current tx
- Persistent: access survives across transactions
And finally, to unseal data off-chain, a user needs two things:
- The contract must have called
FHE.allow(ciphertext, userAddress)
- The user must hold a valid permit that proves they are allowed to unseal that data.
🛠️ How to use it (conceptually)
Here’s the basic lifecycle:
-
Encryption: User encrypts data client-side with
cofhejs.encrypt()
and sends it to the contract. -
Access Granting (Contract): Your contract receives it with
InEuint32
and doesFHE.asEuint32(input)
.- If you’ll use that value in future transactions, you must call
FHE.allowThis(value)
to persist access.
- If you’ll use that value in future transactions, you must call
-
Off-chain Access (User): Before unsealing, the contract must call
FHE.allow(value, user)
and the user must sign a permit usingcofhejs.createPermit()
. -
Unsealing: The user can now decrypt that value with
cofhejs.unseal()
, proving they have a valid permit and access rights.
This access dance ensures that encrypted values:
- Aren’t globally usable by anyone
- Can only be decrypted by the intended party
- Can be safely passed between contracts or across chains (with careful permissioning)
🧵 Example: encrypted balances
Let’s say you’re building a private token or game where users have encrypted scores or balances.
Here’s what you’d want to do:
function transfer(InEuint32 _amount, address to) public {
euint32 amount = FHE.asEuint32(_amount);
balances[msg.sender] = FHE.sub(balances[msg.sender], amount);
balances[to] = FHE.add(balances[to], amount);
// 🔓 Let users unseal their updated balances offchain
FHE.allow(balances[msg.sender], msg.sender);
FHE.allow(balances[to], to);
// ✅ Optional: Let the contract use these values again later
FHE.allowThis(balances[msg.sender]);
FHE.allowThis(balances[to]);
}
On the frontend, a user would generate a permit like this:
const permit = await cofhejs.createPermit({
type: 'self',
issuer: wallet.address,
})
And then unseal their balance:
const unsealed = await cofhejs.unseal(encryptedBalance, FheTypes.Uint32, permit.issuer, permit.getHash());
That’s it — no plaintext exposed, no extra trust assumptions.
📌 Things to remember
-
FHE.asEuint32()
gives you temporary access. For reuse, callFHE.allowThis()
. - To decrypt off-chain, always call
FHE.allow(handle, user)
on the contract side. - Permits are signed proofs that the user owns their encryption keys — required to unseal.
- If you forget
allow()
,unseal()
will silently fail with aSEAL_OUTPUT_RETURNED_NULL
error. - Do not share encrypted handles between users or contracts without explicitly granting permission.
🔎 What makes this unique?
Unlike ZK or MPC systems, FHE data can live on-chain in encrypted form indefinitely — but the only way to extract it is through controlled access.
Rather than hiding inputs or proving secrets once, CoFHE builds a system where:
- Encrypted values are first-class citizens
- Ownership is enforced at the encryption layer
- Access is explicit, granular, and provable
It’s closer to a permissions system for encrypted memory than it is to a traditional "black box compute."
📚 Want to see how to implement it?
This post was a lot of theory, but if you're ready to build something with CoFHE, check out the official docs and step-by-step examples:
👉 Read the CoFHE Access Control Docs
Top comments (0)