Skip to main content

Privacy Protocol

Overview

Lunarys achieves trading privacy through Fully Homomorphic Encryption (FHE) combined with an obfuscation mechanism that allows public price discovery without revealing actual reserve magnitudes.

Fully Homomorphic Encryption (FHE)

What is FHE?

FHE allows computation on encrypted data without decryption. The FHE layer brings this capability to the EVM:

  • Encrypted types: euint64, euint128, ebool -- stored on-chain as ciphertexts
  • Arithmetic: FHE.add, FHE.sub, FHE.mul, FHE.div on encrypted values
  • Comparison: FHE.lt, FHE.le, FHE.ge return ebool
  • Conditional: FHE.select(condition, valueIfTrue, valueIfFalse) -- constant-time
  • RNG: FHE.randEuint16(), FHE.randEuint64() -- on-chain encrypted randomness

How Lunarys Uses FHE

All pool reserves are stored as euint64:

euint64 private reserveA;  // Encrypted -- nobody can read
euint64 private reserveB; // Encrypted -- nobody can read

All swap math operates on these encrypted values:

effectiveIn = FHE.div(FHE.mul(amountIn, effectiveMultiplier), BPS)  // Encrypted
amountOut = Taylor(effectiveIn, obfReserves) // Encrypted
check = FHE.ge(amountOut, minAmountOut) // Encrypted boolean
result = FHE.select(check, amountOut, zero) // Constant-time

Obfuscation Mechanism

The Encryption Lifecycle

flowchart TD
subgraph OnChain["On-Chain (Encrypted)"]
RA["reserveA (euint64)"] --> MUL["FHE.mul(reserve, factor)"]
RB["reserveB (euint64)"] --> MUL
RNG["FHE.randEuint16()"] --> Factor["factor ∈ [1M, ~3M]"]
Factor --> MUL
MUL --> OA["obfReserveA (public)"]
MUL --> OB["obfReserveB (public)"]
end

subgraph UserSide["User Side"]
OA --> Decrypt[Decrypt via FHE gateway]
OB --> Decrypt
Decrypt --> Price["Price ratio = obfA / obfB<br/>(exact, factor cancels)"]
Decrypt --> Magnitude["True magnitude?<br/>Unknown — ~3x uncertainty"]
end

style OnChain fill:#1a202c,color:#fff
style UserSide fill:#2d3748,color:#fff
style RNG fill:#d69e2e,color:#000
style Magnitude fill:#e53e3e,color:#fff
style Price fill:#38a169,color:#fff

The Problem

Users need to know approximate prices to set reasonable slippage. But revealing true reserves would enable MEV attacks.

The Solution: Obfuscated Reserves

The pool exposes obfuscated reserves -- true reserves multiplied by a random factor:

obfuscatedReserveA = reserveA * factor
obfuscatedReserveB = reserveB * factor
obfuscatedLPSupply = totalSupply * factor

Where factor is a random encrypted value in range [1,000,000 to ~2,966,050] (~3x uncertainty).

Why This Works

  1. Price ratio preserved: Since the same factor multiplies both reserves, obfA / obfB = reserveA / reserveB. Users can calculate accurate swap prices.

  2. Magnitude hidden: An observer seeing obfReserveA = 50,000,000,000 knows the true reserve is somewhere between ~16.8M and ~50M tokens. This ~3x uncertainty makes MEV unprofitable.

  3. Fresh per operation: The factor rotates on every swap/liquidity operation. Even if you see two consecutive obfuscated values, you can't determine the factor because it changed.

Factor Generation

Only 3 FHE operations (HCU-optimized):

euint16 seed = FHE.randEuint16();                     // [0, 65535]
euint64 offset = FHE.mul(FHE.asEuint64(seed), step); // seed * 30
euint64 factor = FHE.add(offset, minFactor); // 1,000,000 + offset

Proof System

When users call swap or liquidity functions, they must provide:

  1. Decrypted obfuscated values (clear uint128)
  2. FHE proof that these values match the current on-chain ciphertexts

The pool validates the proof before executing. This ensures users can't submit fake reserve values.

Privacy Guarantees

What Stays Encrypted

DataPrivacy Level
True reserve amountsFully encrypted (euint64)
Trade input amountsFully encrypted
Trade output amountsFully encrypted
LP token balancesFully encrypted (CERC20)
Obfuscation factorFully encrypted

What Is Public

DataVisibility
Pool existencePublic (contract address)
Token pairPublic (assetA, assetB addresses)
Swap directionPublic (A-to-B or B-to-A event)
Participant addressesPublic (sender, recipient)
Price ratioDerivable from obfuscated reserves
Approximate liquidity depth~3x uncertainty range

Attack Resistance

AttackDefense
Front-runningCannot determine exact price impact (3x reserve uncertainty)
Sandwich attacksCannot calculate profitable sandwich bounds
MEV extractionObfuscation makes MEV unprofitable
JIT liquidity0.05% withdrawal fee makes JIT attacks costly
Balance inferenceCERC20 encrypted balances prevent tracking
Activity correlationFactor rotation prevents linking consecutive trades

Concrete Attack Scenarios

Scenario 1: Sandwich Attack

On a traditional DEX, a bot sees a pending 10 ETH swap. It knows the pool has 1,000 ETH / 2.7M USDC. It calculates the exact price impact, front-runs with a buy, lets the victim's trade push the price up, then sells for profit.

On Lunarys: The bot sees obfReserveA = 2,150,000,000 (obfuscated). The true reserve could be anywhere between ~725M and ~2,150M tokens (6 decimals). The bot doesn't know the swap amount either (encrypted). It cannot compute whether the price will move 0.01% or 3%. Any sandwich attempt is a coin flip -- and the 0.3% swap fee on both legs makes it a negative-EV bet.

Scenario 2: Front-Running

A mempool observer spots a atomicSwapAForB transaction. On a public DEX, the observer would see the exact input amount and compute optimal front-running. On Lunarys, the input amount is encrypted (externalEuint64) -- the observer can only see the function selector and the participant addresses. There is nothing to front-run.

Scenario 3: Statistical Reserve Inference

An attacker watches obfuscated reserves over 100 operations, hoping to narrow down the true reserves statistically. Since the factor is drawn uniformly from 65,536 values and rotated independently on every operation, each observation is an independent sample multiplied by an unknown independent random variable. The attacker gains no additional precision beyond the single-observation ~3x window.

Token Standard: CERC20 (ERC7984)

All tokens in Lunarys implement the ERC7984 confidential ERC20 standard:

  • Encrypted balances: Stored as euint64, only the owner can decrypt
  • 6 decimals: All tokens use 6 decimal places
  • Operator model: setOperator(address, uint48 until) grants time-limited spending approval
  • Confidential transfers: confidentialTransferFrom operates on encrypted amounts