Skip to main content

Learning Objectives

By the end of this module, you will:
  1. Understand the gas cost model for FHE operations
  2. Choose the right encrypted type size for each use case
  3. Apply optimization patterns to reduce gas consumption by 30-70%
  4. Profile and benchmark FHE contract gas usage
  5. Identify and eliminate unnecessary encrypted operations

Why Gas Optimization is Critical for FHE

In traditional Solidity, a plaintext addition costs roughly 3-5 gas. An encrypted addition (FHE.add(euint32, euint32)) costs approximately 90,000 gas — a factor of 20,000x more expensive.
Without optimization, a single FHE-heavy transaction can:
  • Cost tens or hundreds of dollars in gas fees on mainnet
  • Exceed the block gas limit (30M) with just 300 operations
  • Drive users away due to poor UX
The good news: there are concrete, repeatable patterns that can reduce your FHE gas costs by 30-70% without changing your contract’s logic.

FHE Gas Cost Reference

Complete Gas Cost Table

Operationeuint8euint16euint32euint64euint128euint256
FHE.add (enc+enc)~50k~65k~90k~130k~180k~250k
FHE.add (enc+plain)~35k~45k~65k~95k~135k~190k
FHE.sub (enc-enc)~50k~65k~90k~130k~180k~250k
FHE.mul (enc*enc)~120k~150k~200k~300k~450k~600k
FHE.mul (enc*plain)~80k~100k~140k~210k~320k~430k
FHE.div (enc/plain)~50k~65k~90k~130k~180k~250k
FHE.select~45k~50k~60k~80k~110k~150k
FHE.eq / FHE.ne~45k~50k~60k~80k~110k~150k
FHE.gt / FHE.lt~45k~50k~60k~80k~110k~150k
FHE.min / FHE.max~90k~100k~120k~160k~220k~300k
Key Observations:
  • Multiplication is 2x more expensive than addition
  • Plaintext operands are 25-35% cheaper than encrypted-encrypted
  • Larger types cost more — euint64 is ~2-3x euint8 cost
  • Bitwise operations are the cheapest (~30-60k)

Optimization Strategy 1: Choose the Right Type Size

This is the simplest and most impactful optimization. If a value is guaranteed to fit in 8 bits, use euint8 instead of euint32 or euint64.

Right-Sized Types

DataRangeBest Type
Age0 – 255euint8
Percentage0 – 100euint8
Year2000 – 2100euint16
Token balance0 – 2^64euint64
Vote count (small DAO)0 – 65535euint16
Boolean flag0 or 1ebool

Code Example

// INEFFICIENT: uses euint64 for age (0-255)
euint64 private _age;

function setAge(uint64 age) external {
    _age = FHE.asEuint64(age);           // ~65k gas for cast
    _age = FHE.add(_age, FHE.asEuint64(1)); // ~130k gas for add
    // Total: ~195k gas
}

Optimization Strategy 2: Use Plaintext Operands

When one operand is a known constant or a value that doesn’t need to be secret, pass it as plaintext. The FHE coprocessor can optimize the circuit when one operand is in the clear.

The Rule

If the second operand is not secret, always pass it as plaintext:
// INEFFICIENT: encrypts a known constant, then adds
euint32 enc = FHE.asEuint32(value);
euint32 ten = FHE.asEuint32(10);     // unnecessary encryption
euint32 result = FHE.add(enc, ten);   // enc+enc: ~90k gas
// Total: ~135k gas (45k cast + 90k add)

Which Operations Support Plaintext Operands?

All of the following accept a plaintext as the second (right-hand) operand:
  • Arithmetic: add, sub, mul, rem
  • Comparison: eq, ne, lt, le, gt, ge
  • Min/Max: min, max
  • Bitwise: and, or, xor
  • Shift/Rotate: shl, shr, rotl, rotr
Division (div) already requires a plaintext divisor — you cannot divide by an encrypted value.

Optimization Strategy 3: Minimize FHE Operations

Every FHE operation has a significant fixed cost. Reducing the total number of operations is often more impactful than optimizing individual ones.

Combine Conditions

// INEFFICIENT: 2 comparisons + 2 selects = 4 FHE ops
ebool tooLow = FHE.lt(enc, minVal);
euint32 step1 = FHE.select(tooLow, minVal, enc);
ebool tooHigh = FHE.gt(step1, maxVal);
euint32 result = FHE.select(tooHigh, maxVal, step1);
// 4 FHE ops: ~240k gas

Pre-compute in Plaintext

Anything that can be computed in plaintext should be computed in plaintext:
// INEFFICIENT: computes discount in encrypted space
euint32 price = FHE.asEuint32(100);
euint32 discountRate = FHE.asEuint32(10);
euint32 discountAmount = FHE.mul(price, discountRate);
euint32 finalPrice = FHE.sub(price, FHE.div(discountAmount, 100));
// 4 FHE ops!

Optimization Strategy 4: Batch Processing

In traditional Solidity, individual transactions are cheap. With FHE, each transaction has base overhead (21,000 gas + contract call overhead). Batching multiple updates into a single transaction amortizes this overhead.

Example: Updating Multiple Balances

// INEFFICIENT: three separate transactions
function updateBalanceA(uint32 amount) external {
    _balanceA = FHE.add(_balanceA, amount);
    FHE.allowThis(_balanceA);
    FHE.allow(_balanceA, msg.sender);
}
// 3 transactions: 3 x 21k base + 3 x FHE ops

Optimization Strategy 5: Caching and Storage Trade-offs

If an encrypted value is computed from other encrypted values, and those source values don’t change often, cache the result instead of recomputing it every time.

Example: Tax Rate Calculation

// INEFFICIENT: recomputes tax rate every call
function applyTax(uint32 price) external {
    euint32 baseTax   = FHE.asEuint32(10);
    euint32 surcharge = FHE.asEuint32(5);
    euint32 taxRate   = FHE.add(baseTax, surcharge); // recomputed!
    euint32 encPrice  = FHE.asEuint32(price);
    _result = FHE.mul(encPrice, taxRate);
    // 4 FHE ops: ~375k gas
}

The Storage Trade-off

Storing an encrypted value in a state variable (SSTORE) costs gas too — roughly 20,000 gas for a new slot, 5,000 for an update. The trade-off:
ScenarioBetter Approach
Value recomputed 1-2 timesRecompute (storage cost not worth it)
Value recomputed 3+ timesCache it (amortized savings exceed storage cost)
Value changes every blockRecompute (cache invalidated immediately)
Value changes rarelyCache it (big win)

Optimization Strategy 6: Lazy Evaluation

Lazy evaluation means deferring an expensive computation until its result is actually needed. If the result may never be needed, you save the cost entirely.

Example: Deferred Squaring

// INEFFICIENT: computes square immediately on every update
function updateValue(uint32 value) external {
    euint32 enc = FHE.asEuint32(value);
    euint32 squared = FHE.mul(enc, enc);    // expensive!
    _result = FHE.add(squared, 1);
    // 3 FHE ops every call
}
If updateValue is called 10 times before computeResult is called once, you save 9 multiplications worth of gas (9 x ~200k = ~1.8M gas saved).

Gas Profiling Technique

You cannot optimize what you cannot measure. Here’s how to profile FHE gas usage in your Hardhat tests.

Using receipt.gasUsed

it("should measure gas for an FHE operation", async function () {
  const tx = await contract.someFunction(42);
  const receipt = await tx.wait();
  console.log(`Gas used: ${receipt.gasUsed}`);
});

Comparing Two Implementations

it("should compare inefficient vs optimized", async function () {
  const tx1 = await contract.inefficient_version(42);
  const receipt1 = await tx1.wait();

  const tx2 = await contract.optimized_version(42);
  const receipt2 = await tx2.wait();

  const savings = receipt1.gasUsed - receipt2.gasUsed;
  const pct = (Number(savings) * 100) / Number(receipt1.gasUsed);
  console.log(`Savings: ${savings} gas (${pct.toFixed(1)}%)`);
});

Setting Gas Budgets

For production contracts, establish maximum gas budgets per function:
it("transfer should use less than 500k gas", async function () {
  const tx = await contract.transfer(recipient, 100);
  const receipt = await tx.wait();
  expect(receipt.gasUsed).to.be.lessThan(500_000n);
});
This acts as a regression test: if a future refactor accidentally adds FHE operations, the test will fail.

Real-World Example: Optimizing a Confidential ERC-20 Transfer

Let’s walk through a realistic optimization of a confidential token transfer function.
function transfer(address to, uint32 amount) external {
    euint32 encAmount = FHE.asEuint32(amount);

    // Check balance >= amount (encrypted comparison)
    ebool hasEnough = FHE.ge(_balances[msg.sender], encAmount);

    // Compute new balances
    euint32 newSenderBal   = FHE.sub(_balances[msg.sender], encAmount);
    euint32 newReceiverBal = FHE.add(_balances[to], encAmount);

    // Conditionally apply (if balance was sufficient)
    _balances[msg.sender] = FHE.select(hasEnough, newSenderBal, _balances[msg.sender]);
    _balances[to]         = FHE.select(hasEnough, newReceiverBal, _balances[to]);

    // ACL
    FHE.allowThis(_balances[msg.sender]);
    FHE.allow(_balances[msg.sender], msg.sender);
    FHE.allowThis(_balances[to]);
    FHE.allow(_balances[to], to);
}
// Operations: 1 cast + 1 ge + 1 sub + 1 add + 2 select = 6 FHE ops
// Estimated gas: ~45k + ~60k + ~90k + ~90k + ~120k = ~405k
function transfer(address to, uint32 amount) external {
    // Use plaintext operand for the amount (no cast needed for comparisons/arithmetic)
    ebool hasEnough = FHE.ge(_balances[msg.sender], amount);  // enc vs plain

    euint32 newSenderBal   = FHE.sub(_balances[msg.sender], amount);  // enc - plain
    euint32 newReceiverBal = FHE.add(_balances[to], amount);           // enc + plain

    _balances[msg.sender] = FHE.select(hasEnough, newSenderBal, _balances[msg.sender]);
    _balances[to]         = FHE.select(hasEnough, newReceiverBal, _balances[to]);

    FHE.allowThis(_balances[msg.sender]);
    FHE.allow(_balances[msg.sender], msg.sender);
    FHE.allowThis(_balances[to]);
    FHE.allow(_balances[to], to);
}
// Operations: 1 ge(plain) + 1 sub(plain) + 1 add(plain) + 2 select = 5 FHE ops
// Estimated gas: ~45k + ~65k + ~65k + ~120k = ~295k
// Savings: ~110k gas (27%)
The only change: we removed the FHE.asEuint32(amount) cast and used the plaintext amount directly as the second operand. This eliminated the cast entirely and made the arithmetic operations cheaper.

Optimization Checklist

Use this checklist when reviewing your FHE contracts:
1

Right-sized types?

Are you using the smallest encrypted type that fits your data?
2

Plaintext operands?

For every FHE operation, is the second operand truly secret? If not, pass it as plaintext.
3

Minimum operations?

Can any FHE operations be eliminated by refactoring or pre-computing in plaintext?
4

Batch updates?

Are there multiple state changes that can be combined into a single transaction?
5

Cached intermediates?

Are there encrypted values being recomputed that could be stored?
6

Lazy evaluation?

Are there expensive computations that can be deferred?
7

Redundant comparisons?

Can gt + select be replaced with max? Can lt + select be replaced with min?
8

Unnecessary casts?

Are you calling FHE.asEuintXX() on values that could be passed as plaintext operands directly?
9

Gas budget tests?

Do your tests verify that key functions stay within a gas budget?
10

Profiled?

Have you measured actual gas usage with receipt.gasUsed?

Common Anti-Patterns

Anti-Pattern 1: “Encrypt Everything”

// BAD: encrypts the contract owner address check
ebool isOwner = FHE.eq(FHE.asEuint32(uint32(uint160(msg.sender))),
                       FHE.asEuint32(uint32(uint160(owner))));

// GOOD: owner is public, use plaintext
require(msg.sender == owner, "Not owner");

Anti-Pattern 2: “One Operation Per Transaction”

// BAD: separate transactions for each step
function step1_addBalance(uint32 amt) external { ... }
function step2_checkLimit() external { ... }
function step3_applyFee() external { ... }

// GOOD: combine into one
function updateBalance(uint32 amt) external {
    // add + check + fee in one transaction
}

Anti-Pattern 3: “Ignoring Type Sizes”

// BAD: using euint256 for a boolean flag
euint256 private _isActive;

// GOOD: use ebool
ebool private _isActive;

Anti-Pattern 4: “Recomputing Constants”

// BAD: encrypts "100" every time the function is called
function applyPercent(euint32 value) internal returns (euint32) {
    return FHE.div(FHE.mul(value, FHE.asEuint32(percentage)), FHE.asEuint32(100));
}

// GOOD: use plaintext for the constant 100
function applyPercent(euint32 value) internal returns (euint32) {
    return FHE.div(FHE.mul(value, percentage), 100);
}

Summary of Gas Savings by Pattern

PatternTypical SavingsDifficulty
Right-sized types30-60%Easy
Plaintext operands15-35%Easy
Minimize operations20-80%Medium
Batch processing10-20% (tx overhead)Easy
Caching25-50% (on repeated calls)Medium
Lazy evaluation0-90% (depends on access pattern)Hard
Redundant select elimination10-30%Medium
Avoid unnecessary casts5-15%Easy

Key Takeaways

FHE operations cost 10-100x more

Optimization is not optional — it’s essential for practical FHE contracts.

Use plaintext operands

The single biggest win — requires zero architectural changes.

Right-size types

Second easiest win — use euint8 when 8 bits suffice.

Measure gas

Always measure with receipt.gasUsed before and after optimizing.
Before encrypting a value, ask yourself: “Does this actually need to be secret?”

Reference Contracts

  • GasOptimized.sol (contracts/15-gas-optimization/GasOptimized.sol:1) — Before/after optimization patterns
  • GasBenchmark.sol (contracts/15-gas-optimization/GasBenchmark.sol:1) — Isolated benchmarks for individual FHE operations

Next Steps

In the exercise for this module, you’ll be given a deliberately inefficient confidential token contract. Your task is to apply the patterns from this lesson to reduce its gas consumption by at least 30%. Next: Module 16: Security Best Practices →

Build docs developers (and LLMs) love