Skip to main content

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

ConstantValuePurpose
SCALE1e18Fixed-point scaling for price computation
MAX_OBFUSCATION_FACTOR3,000,000Conservative upper bound (actual max ~2,966,050)
MAX_TAYLOR_EPS_DENOMINATOR16Max 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
);