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.divon encrypted values - Comparison:
FHE.lt,FHE.le,FHE.gereturnebool - 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
-
Price ratio preserved: Since the same factor multiplies both reserves,
obfA / obfB = reserveA / reserveB. Users can calculate accurate swap prices. -
Magnitude hidden: An observer seeing
obfReserveA = 50,000,000,000knows the true reserve is somewhere between ~16.8M and ~50M tokens. This ~3x uncertainty makes MEV unprofitable. -
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:
- Decrypted obfuscated values (clear
uint128) - 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
| Data | Privacy Level |
|---|---|
| True reserve amounts | Fully encrypted (euint64) |
| Trade input amounts | Fully encrypted |
| Trade output amounts | Fully encrypted |
| LP token balances | Fully encrypted (CERC20) |
| Obfuscation factor | Fully encrypted |
What Is Public
| Data | Visibility |
|---|---|
| Pool existence | Public (contract address) |
| Token pair | Public (assetA, assetB addresses) |
| Swap direction | Public (A-to-B or B-to-A event) |
| Participant addresses | Public (sender, recipient) |
| Price ratio | Derivable from obfuscated reserves |
| Approximate liquidity depth | ~3x uncertainty range |
Attack Resistance
| Attack | Defense |
|---|---|
| Front-running | Cannot determine exact price impact (3x reserve uncertainty) |
| Sandwich attacks | Cannot calculate profitable sandwich bounds |
| MEV extraction | Obfuscation makes MEV unprofitable |
| JIT liquidity | 0.05% withdrawal fee makes JIT attacks costly |
| Balance inference | CERC20 encrypted balances prevent tracking |
| Activity correlation | Factor 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:
confidentialTransferFromoperates on encrypted amounts