Swap Mechanism
Overview
The Lunarys eDEX swap mechanism uses atomic single-transaction execution. Users call the EPOOL directly with encrypted inputs and pre-decrypted obfuscated reserves. The pool validates proofs, computes the output via a 4-term Taylor approximation, and either completes the swap or refunds the input -- all in one transaction.
Function Signature
function atomicSwapAForB(
externalEuint64 amountInExt, // Encrypted input amount
externalEuint64 minAmountOutExt, // Encrypted minimum output (slippage)
bytes calldata proofIn, // FHE proof for encrypted inputs
address recipient, // Output token recipient
uint128 decryptedORA, // Decrypted obfuscated reserve A
uint128 decryptedORB, // Decrypted obfuscated reserve B
bytes calldata reserveProof // FHE proof for reserve values
) external
atomicSwapBForA has the same signature but swaps in the opposite direction.
Swap Lifecycle
sequenceDiagram
participant User
participant SDK as FHE SDK
participant Gateway as FHE Gateway
participant Pool as EPOOL
Note over User,Pool: Phase 1 — Off-Chain Preparation
User->>Pool: obfuscatedStates()
Pool-->>User: (obfReserveA, obfReserveB, lpSupply)
User->>Gateway: decrypt([obfReserveA, obfReserveB])
Gateway-->>User: (clearA, clearB, proof)
User->>SDK: createEncryptedInput(amountIn, minAmountOut)
SDK-->>User: (encryptedHandles, inputProof)
Note over User,Pool: Phase 2 — On-Chain Execution
User->>Pool: atomicSwapAForB(encrypted, proof, reserves, reserveProof)
Pool->>Pool: Validate reserveProof vs. ciphertexts
Pool->>Pool: Pull amountIn from user
Pool->>Pool: effectiveIn = amountIn × (1 − fee)
Pool->>Pool: amountOut = Taylor(effectiveIn, obfReserves)
alt Both checks pass
Pool->>User: Transfer amountOut
Pool->>Pool: Add amountIn to reserves
else Taylor bound or slippage fails
Pool->>User: Refund amountIn
end
Pool->>Pool: Rotate obfuscation factor (fresh RNG)
Step-by-Step Flow
1. User Preparation (Off-Chain)
a) Fetch obfuscated reserves:
pool.obfuscatedStates() -> (obfReserveA, obfReserveB, lpSupply)
b) Decrypt via FHE gateway:
gateway.decrypt([obfReserveA, obfReserveB]) -> (clearA, clearB, proof)
c) Encrypt swap amount:
fhe.createEncryptedInput(poolAddress, userAddress)
input.add64(amountIn)
input.add64(minAmountOut)
encrypted = input.encrypt()
2. On-Chain Execution
a) Pool validates reserveProof against current obfuscatedStates ciphertexts
b) Pool pulls amountIn from user via confidentialTransferFrom
c) Pool computes swap via SwapLib:
- effectiveIn = amountIn * (BPS_DENOMINATOR - swapFeeBps) / BPS_DENOMINATOR
- amountOut = computeConstantProductOutput(effectiveIn, obfIn, obfOut)
d) Pool checks:
- Taylor bound: effectiveIn <= maxEffectiveIn (prevents oversized swaps)
- Slippage: amountOut >= minAmountOut
e) If BOTH checks pass:
- Transfer amountOut to recipient
- Add full amountIn to reserves (fee stays implicitly)
If EITHER fails:
- Refund amountIn to caller
f) Rotate obfuscation factor (fresh RNG)
Pricing: 4-Term Taylor Approximation
Why Taylor?
The exact constant-product formula requires encrypted division:
amountOut = effectiveIn * reserveOut / (reserveIn + effectiveIn)
Encrypted division by encrypted values is extremely expensive. Instead, we use a Taylor expansion with clear obfuscated values for the denominators (factor cancels in ratios):
Constants
| Constant | Value | Purpose |
|---|---|---|
SCALE | 1e18 | Fixed-point scaling for price computation |
MAX_OBFUSCATION_FACTOR | 3,000,000 | Conservative upper bound (actual max ~2,966,050) |
MAX_TAYLOR_EPS_DENOMINATOR | 16 | Max swap = reserveLowerBound / 16 (~6.25%) |
Formula
price = obfOut * SCALE / obfIn (clear math -- factor cancels)
reserveInLowerBound = obfIn / MAX_OBFUSCATION_FACTOR
x = effectiveIn / reserveInLowerBound
amountOut = effectiveIn * price * (1 - x + x^2 - x^3)
This is a 4-term alternating series which guarantees:
- No overpayment: Approximation is always less than or equal to exact output
- Constant product invariant preserved: Pool never gives out more than it should
Implementation Optimization
All Taylor coefficients are computed using cleartext values (maxEffectiveIn, reserveInLowerBound) -- only one FHE multiplication and one FHE division are needed per swap. The coefficients are combined into a single combinedCoeff in cleartext, then:
result = FHE.mul(effectiveIn, combinedCoeff) // 1 FHE mul
amountOut = FHE.div(result, SCALE) // 1 FHE div
Accuracy
| Swap Size (% of reserve) | Approximate Underpayment |
|---|---|
| 1% | ~0.01% |
| 5% | ~0.25% |
| 6.25% (max) | ~1% |
Max Swap Size
maxEffectiveIn = reserveInLowerBound / 16
This limits swaps to ~6.25% of the conservative reserve estimate. Larger trades must be split into multiple transactions.
Fee Model
Swap Fee
Deducted from input before pricing:
effectiveIn = amountIn * (1,000,000 - swapFeeBps) / 1,000,000
The full amountIn (including fee portion) goes to reserves. LPs realize fees when removing liquidity -- Uniswap V2 style.
Example
Pool with 0.3% fee (3,000 BPS):
User swaps 1,000 tokens
effectiveIn = 1,000 * (1,000,000 - 3,000) / 1,000,000 = 997 tokens
Output computed on 997 tokens
Full 1,000 tokens added to reserves
Slippage Protection
The encrypted minAmountOut parameter provides on-chain slippage protection:
ebool slippageOk = FHE.ge(amountOut, minAmountOut); // Encrypted comparison
// If slippage check fails, swap is refunded
This is computed entirely on encrypted values -- the pool never learns the user's slippage tolerance.
Security Considerations
Proof Verification
The pool validates that decryptedORA and decryptedORB match the current on-chain ciphertexts via FHE signatures. This prevents:
- Stale reserves (from a previous state)
- Manipulated reserve values
Reentrancy Protection
All swap functions use OpenZeppelin's ReentrancyGuard.
Atomic Refund
If the Taylor bound or slippage check fails, the input amount is refunded to the caller in the same transaction. No funds are ever stuck.
Events
event AtomicSwapExecuted(
address indexed caller,
address indexed recipient,
bool aForB,
bytes32 amountOutHandle
);