Fhenix × Consensys Diligence: Securing FHE Smart Contracts

When a Contract Cannot See Its Own State

Modified:
20 October 2026
Published:
20 October 2026

Most of what we know about writing secure smart contracts rests on a fundamental assumption: a contract can read its own state. A simple require(balance >= amount) is so deeply embedded in how token transfers work that we rarely stop to question it.

Fully homomorphic encryption (FHE) changes that assumption entirely, introducing a new set of possibilities and security considerations for onchain applications.

As part of our engagement with @ConsensysAudits, we explored these challenges across several implementations built using Fhenix's confidential computation model: confidential tokens supporting both public and private balances, a vault issuing encrypted shares, an over-the-counter trading desk, and a sealed-bid auction capable of reaching a clearing price without revealing individual bids, even to the contract itself.

These implementations demonstrate what's possible when smart contracts can compute on encrypted data without ever accessing the underlying values. But they also introduce a different set of security considerations, requiring developers to rethink familiar approaches to execution, accounting, and authorization.

This article explores what changes when smart contracts can no longer see the data they're working with, and the security principles that make confidential computation possible.

Inside CoFHE: How Fhenix Executes on Encrypted Data

The first thing to understand is that nothing encrypted is computed on chain.

When a contract calls something like FHE.mul(a, b), no multiplication happens in the EVM. The call records a task and immediately returns a 32-byte handle standing in for the result. The real work happens off chain, in a coprocessor that holds the actual ciphertexts. You can picture it as maintaining a table mapping each handle to a real encrypted value. The chain holds the handles and the shape of the computation. The coprocessor holds the data.

The handle is not random. It is derived deterministically from the operation and its inputs, roughly a hash of the operands together with the opcode, with some type metadata packed into the low bytes. That detail matters more than it first appears. Because the handle is a pure function of what produced it, the contract can work it out on the spot, without waiting for the coprocessor to catch up. It gets a usable reference to a result that does not exist yet, and can keep building on it in the same transaction.

So on-chain execution is really the construction of a computation graph. Each operation appends a node and hands back its address. What the chain pays for is how many operations you queue, never what is inside them, because it never touches what is inside them.

Reading a value back is a separate and deliberate act. A dedicated access control contract records which addresses may decrypt which handles. Holding a handle grants nothing at all, since it is only a pointer and anyone can compute one. Permission lives entirely in that separate ledger. When someone with permission wants a plaintext, the decryption happens off chain and returns as a signed result the contract can verify before acting on it.

Three things follow from this arrangement, and everything below descends from them.

1/ The contract cannot branch on encrypted data.

Comparing two encrypted numbers gives you an encrypted boolean. The EVM cannot read it, so it cannot be used in an if or a require.

2/ The contract cannot fail on encrypted data.

Since it cannot branch, it cannot revert. A confidential transfer of more than you own does not throw. It returns.

3/ Holding a value and being entitled to it are separate questions.

The handle is just a pointer, anyone can compute one, and entitlement lives in a different place entirely.

The first two shape how logic and error handling work. The third shapes who is allowed to touch what. The second in particular sounds like a limitation, and is closer to a requirement.

Principle one: failure has to be silent, or it leaks

Consider what a revert actually is. It is a one-bit signal, broadcast to everyone, about whether a condition held.

In a public token that is fine. Everyone can already see your balance. But in a confidential system, a transaction that reverts when you are short of funds tells the entire network that you were short of funds. The privacy guarantee would be undone by the error handling.

So confidential transfers do not revert on insufficient balance. They compute both outcomes and pick one without knowing which:

success = FHE.gte(oldValue, delta);

updated =

FHE.select

(success, FHE.sub(oldValue, delta), oldValue);

Both branches are evaluated, and the discarded one is computed just as fully as the kept one. The contract never learns which was which. This small helper sits underneath the whole token layer, and every confidential balance change in the system routes through it.

The same silence applies to arithmetic. Solidity has reverted on overflow since 0.8, but encrypted arithmetic cannot: there is no way to check a result the contract cannot read, and reverting would leak that a bound was crossed. Encrypted values wrap quietly. Any bound worth enforcing has to be enforced by construction, through the choice of widths and scaling factors, rather than caught after the fact.

This is the shape of nearly all confidential logic: not a decision, but a set of conditional futures collapsed by a value nobody on chain can read. The contract queues the same operations either way, and finds out nothing.

Principle two: success becomes a value you have to inspect

If a transfer cannot revert, then "the call did not revert" tells you nothing about whether money moved.

This is the single biggest adjustment for anyone coming from ERC-20. There, a transfer either succeeds or the transaction dies. Here, a transfer always succeeds, and separately reports how much it actually moved, as an encrypted number. Which means every consumer of a confidential transfer has to compare what it asked for against what it got. The auction does this on the way in, when it pulls payment for a bid:

euint64 eTotalPaid = paymentToken.confidentialTransferFrom(msg.sender, address(this), ePaid);

ebool eIsPaymentConfirmed = FHE.eq(eTotalPaid, ePaid);

...

eQuantity =

FHE.select

(eIsPaymentConfirmed, eQuantity, FHE.asEuint64(0));

Notice what it does when payment falls short. It does not revert, because that would leak. It nullifies, setting the quantity to zero so the record exists but counts for nothing. The transaction succeeds, the books stay consistent, and no observer learns why.

That is the idiom worth internalising: verify the returned amount, then neutralise rather than reject.

Paying out is harder. Code that clears an internal debt and then transfers is correct only as long as the token reverts on failure. In a public ERC-20 it does, and the revert takes the state change with it. A confidential transfer does not revert. It quietly moves whatever is available, so the debt can be cleared while nothing moved.

What makes that pattern safe here is not a check but an invariant: money only leaves because money came in, and the bookkeeping is what guarantees the balance is there. Solvency stops being something the runtime enforces and becomes something the accounting has to establish. The guarantee no longer lives in a line you can point at, it lives in the relationship between every credit and every debit, and a later change that breaks it will not announce itself.

Adding a check does not help as much as you would hope. You can compare what was delivered against what was requested, but the answer comes back encrypted, and the thing you would want to skip is an ordinary public write: a boolean saying this user has been refunded. If that boolean were only set when the transfer succeeded, anyone could read it and learn whether it succeeded, which is the fact you were hiding.

The rule generalises: anything that depends on a secret has to stay secret. A shortfall cannot simply stop a public flag being set. It has to be recorded as an encrypted amount still owed, for some later call to pay out.

So the same code that is safe in a public token is safe here for a completely different reason. There, the transfer itself refuses to overdraw. Here, nothing refuses anything, and the safety rests entirely on the accounting being right.

Principle three: a handle is not a claim

Encrypted values reach a contract two very different ways, and conflating them is easy.

A user submitting a new value sends a ciphertext together with a proof they produced it. Converting that input verifies the proof and binds the value to the sender, so the contract knows the caller genuinely owns what they submitted.

A function that instead accepts a bare handle accepts something quite different: a pointer to a value that already exists somewhere in the system. Handles are deterministic, and they routinely appear in event logs and calldata, so anyone can obtain one. Possessing a handle proves nothing whatsoever about entitlement to the value behind it. So any function taking an existing handle has to ask the permission ledger explicitly whether this caller is allowed to use it.

Now suppose one does not. A function accepts an encrypted amount and gets to work without ever checking who is asking. I find Alice's balance handle in an event log and pass it in. Whatever the function computes, some of the result lands in my account, and I am allowed to read my own account. I decrypt my balance before the call and after it. The difference is Alice's balance. I never read her handle. The contract never decrypted anything it should not have. Every permission check that ran, passed. The value simply travelled somewhere I already had the right to look, and arithmetic carries information even when nobody can see it.

The nearest ERC-20 equivalent would be a transferFrom that never consults the allowance, which nobody would ship. This version is much easier to overlook, because the value looks opaque and therefore feels protected. Encryption protects the contents. It does nothing to establish who is asking.

Which is why this cannot be left to review. Every function that accepts an existing handle needs a deliberate decision about who is entitled to pass one, made when the interface is designed rather than caught afterwards. A missing authorisation check in a public contract tends to surface quickly, because the values are visible and the damage is legible. Here it can sit in a function that looks entirely correct, operating on data nobody can inspect, until someone works out which way the arithmetic flows.

What Auditing Confidential Contracts Demands Differently

Reviewing confidential smart contracts requires a different way of thinking about security. The familiar questions still matter, but FHE introduces an entirely new set of considerations: what can a contract know, what must remain encrypted, and how can it enforce rules when it cannot read the values it's working with?

Our engagement with Consensys Diligence explored exactly these questions, revealing how traditional assumptions around execution, accounting, and authorization need to evolve when confidentiality becomes part of the computation itself.

The possibilities are significant. Markets can reach clearing prices without exposing individual bids. Contracts can execute transactions, enforce conditions, and produce verifiable outcomes without ever accessing the underlying plaintext. Applications that previously had to choose between confidentiality and onchain functionality can now support both.

Building these systems securely requires more than adapting existing smart contract patterns. It calls for rethinking how they're designed from the ground up.

Our collaboration with Consensys Diligence is a step toward understanding that new design space, and we're excited to see what developers build with capabilities that weren't possible before.

‍

Confidential Computing for

the Next Wave of DeFi

Join developers and protocols building the next generation of

onchain applications — powered by encrypted execution.