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​

Swap Lifecycle

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
);