Skip to main content

Swap Mechanism Deep Dive

Overview

The Lunarys DEX swap mechanism is a fully confidential automated market maker (AMM) that maintains privacy throughout the entire swap lifecycle. Unlike traditional AMMs where all amounts and reserves are public, Lunarys keeps everything encrypted using Fully Homomorphic Encryption (FHE).

Two-Phase Swap Architecture

Swaps in Lunarys follow a unique two-phase process due to the confidential nature of the system:

Phase 1: Swap Initiation (swapExactAForB / swapExactBForA)

  • User submits encrypted swap amount
  • Pool pulls encrypted tokens from user
  • Encrypted reserve calculations performed
  • Decryption request sent to oracle
  • PendingSwap struct stored

Phase 2: Swap Settlement (settleSwap)

  • Oracle provides decrypted values
  • Final output calculated from decrypted data
  • Tokens transferred to recipient
  • Reserves updated
  • Swap completed

This two-phase design is necessary because:

  1. FHE allows computation on encrypted data
  2. But final token transfers require knowing exact amounts
  3. Decryption must be done securely via threshold oracle
  4. The pool bridges encrypted and plaintext worlds

Phase 1: Swap Initiation

Function Signatures

function swapExactAForB(
externalEuint64 amountInExt,
bytes calldata proofIn,
address recipient,
bytes calldata hookContext
) external ensureInitialized nonReentrant returns (uint256 requestID)

function swapExactBForA(
externalEuint64 amountInExt,
bytes calldata proofIn,
address recipient,
bytes calldata hookContext
) external ensureInitialized nonReentrant returns (uint256 requestID)

Parameters Explained

ParameterTypeDescription
amountInExtexternalEuint64Encrypted input amount - Created by user's wallet using FHEVM SDK
proofInbytesZero-knowledge proof - Proves user knows the encrypted value without revealing it
recipientaddressDestination address - Where output tokens will be sent
hookContextbytesOptional hook data - Passed to beforeSwap/afterSwap hooks if configured

Step-by-Step Process

Step 1: Pull Encrypted Tokens

euint64 rawAmountIn = FHE.fromExternal(amountInExt, proofIn);
euint64 pulled = _pullIntoPool(assetA, rawAmountIn);

What happens:

  1. Convert external encrypted input to internal FHE ciphertext
  2. Verify the zero-knowledge proof
  3. Call confidentialTransferFrom to pull tokens
  4. Return the actually pulled amount (might differ from requested)

Why it matters:

  • User might not have enough balance
  • The pulled amount becomes the actual swap amount
  • Everything stays encrypted

Step 2: Apply Swap Fee

function _applyFee(euint64 amount) private returns (euint64) {
euint64 feeMultiplier = FHE.asEuint64(BPS_DENOMINATOR - swapFee);
euint64 feeDenominator = FHE.asEuint64(BPS_DENOMINATOR);
return FHE.div(FHE.mul(amount, feeMultiplier), feeDenominator);
}

Example:

  • Swap fee: 0.3% = 3000 basis points
  • BPS_DENOMINATOR = 1,000,000
  • feeMultiplier = 1,000,000 - 3,000 = 997,000
  • amountAfterFee = (amountIn × 997,000) / 1,000,000

All calculations done on encrypted values!

Step 3: Calculate New Reserves (Encrypted)

// Constant product formula: x * y = k
euint64 amountAfterFee = _applyFee(pulled);
euint64 newReserveIn = FHE.add(reserveInBefore, amountAfterFee);

// Calculate liquidity constant
euint128 constantProduct = FHE.mul(reserveInBefore, reserveOutBefore);

Key insight:

  • All arithmetic (add, mul, div) done on encrypted values
  • Result is still encrypted
  • No one knows the actual numbers

Step 4: Prepare for Decryption (Masked Values)

This is the most critical part for privacy:

// Generate random mask
euint128 randomMask = FHE.randEuint128();

// Apply mask to constant product
euint128 maskedNumerator = FHE.mul(constantProduct, randomMask);

// Mask the denominator
euint128 maskedDenominator = FHE.asEuint128(FHE.mul(newReserveIn, randomMask));

// Request decryption of ONLY the masked denominator
bytes32 maskedDenominatorHandle = FHE.toBytes32(maskedDenominator);
uint256 requestID = _requestDecryption(maskedDenominatorHandle);

Why masking is crucial:

Without masking, decrypting newReserveIn would reveal:

  • The exact reserve amounts
  • The pool's pricing ratio
  • Allow front-running attacks

With masking:

  • Oracle decrypts maskedDenominator = newReserveIn × randomMask
  • Pool divides maskedNumerator / decrypted_maskedDenominator
  • Mask cancels out: (k × mask) / (newReserveIn × mask) = k / newReserveIn
  • Privacy preserved!

Step 5: Store Pending Swap

pendingSwaps[requestID] = PendingSwap({
exists: true,
aForB: true,
caller: msg.sender,
recipient: recipient,
amountInValue: pulled,
reserveInBefore: reserveInBefore,
reserveOutBefore: reserveOutBefore,
newReserveIn: newReserveIn,
maskedNumerator: maskedNumerator,
hookContext: hookContext,
amountInHandle: pulledHandle,
maskedDenominatorHandle: maskedDenominatorHandle
});

This struct preserves all encrypted state needed for settlement.

Step 6: Emit Event & Call Hook

emit SwapDecryptionRequested(requestID, msg.sender, recipient, aForB, maskedDenominatorHandle);

if (hasSwapHook) {
hooks.swapHook.beforeSwap(msg.sender, aForB, pulledHandle, hookData, hookContext);
}

Phase 2: Swap Settlement

Function Signature

function settleSwap(
uint256 requestID,
bytes memory cleartexts,
bytes memory decryptionProof
) external nonReentrant

Parameters

ParameterTypeDescription
requestIDuint256The pending swap ID to settle
cleartextsbytesDecrypted values from the oracle
decryptionProofbytesCryptographic proof that decryption is valid

Settlement Process

Step 1: Verify Decryption Proof

FHE.checkSignatures(requestID, cleartexts, decryptionProof);

What this does:

  • Verifies the oracle's threshold signature
  • Ensures the decrypted values match the requested ciphertexts
  • Prevents tampering or fake settlements

If verification fails:

  • Transaction reverts
  • Swap remains pending
  • Can be retried later

Step 2: Decode Decrypted Values

(uint256 denominatorRaw, uint256 liquidityRaw) = abi.decode(cleartexts, (uint256, uint256));

The oracle decrypts:

  1. denominatorRaw = decrypted maskedDenominator
  2. liquidityRaw = decrypted liquidity constant (optional)

Step 3: Check for Zero Liquidity (Refund Case)

if (liquidityRaw == 0) {
_refundSwap(pending);
emit SwapRejected(requestID, pending.caller, pending.aForB);
delete pendingSwaps[requestID];
return;
}

Why refund?

  • Pool might have been drained
  • Invalid state detected
  • Protect user from failed swap

Refund process:

function _refundSwap(PendingSwap storage pending) private {
if (pending.aForB) {
assetA.confidentialTransfer(pending.caller, pending.amountInValue);
} else {
assetB.confidentialTransfer(pending.caller, pending.amountInValue);
}
}

User gets their tokens back (encrypted).

Step 4: Calculate Output Amount

uint128 decryptedDenominator = uint128(denominatorRaw);

// Remember: maskedNumerator = constantProduct × randomMask
// decryptedDenominator = newReserveIn × randomMask
// So: maskedNumerator / decryptedDenominator = constantProduct / newReserveIn = newReserveOut

euint64 reserveOutNew = FHE.asEuint64(
FHE.div(pending.maskedNumerator, decryptedDenominator)
);

euint64 amountOut = FHE.sub(pending.reserveOutBefore, reserveOutNew);

The magic:

  • Divide masked values
  • Random mask cancels out
  • Get correct new reserve
  • Subtract to find output amount
  • Result is still encrypted!

Step 5: Update Reserves

if (pending.aForB) {
reserveA = pending.newReserveIn;
reserveB = reserveOutNew;
reserveATrace.push(block.number, reserveA);
reserveBTrace.push(block.number, reserveB);
} else {
reserveB = pending.newReserveIn;
reserveA = reserveOutNew;
reserveATrace.push(block.number, reserveA);
reserveBTrace.push(block.number, reserveB);
}

lastUpdate = uint32(block.timestamp);

**Important:**Checkpoints are updated (explained in detail below).

Step 6: Transfer Output Tokens

IERC7984 assetOut = pending.aForB ? assetB : assetA;
assetOut.confidentialTransfer(pending.recipient, amountOut);

Encrypted output amount sent to recipient.

Step 7: Call After-Swap Hook

if (hasSwapHook) {
hooks.swapHook.afterSwap(
pending.caller,
pending.recipient,
pending.aForB,
pending.amountInHandle,
amountOutHandle,
hookData,
pending.hookContext
);
}

Hooks can:

  • Track trading volume
  • Update LP positions
  • Distribute fees
  • Analytics

Step 8: Emit Events & Cleanup

emit SwapSettled(requestID, pending.caller, pending.recipient, pending.aForB, amountOutHandle);
emit SwapExecuted(pending.caller, pending.recipient, pending.aForB);

delete pendingSwaps[requestID];

Constant Product Formula (Encrypted)

Traditional AMM

x * y = k  (constant product)

Where:

  • x = reserve of token X
  • y = reserve of token Y
  • k = liquidity constant

Confidential AMM

euint64(x) * euint64(y) = euint128(k)

All operations performed on encrypted values using FHE.

Swap Calculation

Given:

  • reserveIn = encrypted reserve of input token
  • reserveOut = encrypted reserve of output token
  • amountIn = encrypted input amount
  • fee = swap fee (e.g., 0.3%)

Calculate:

1. amountInWithFee = amountIn × (1 - fee)
2. newReserveIn = reserveIn + amountInWithFee
3. constantProduct = reserveIn × reserveOut
4. newReserveOut = constantProduct / newReserveIn
5. amountOut = reserveOut - newReserveOut

All steps encrypted except step 4 which uses masked decryption!


Privacy Guarantees

What Remains Private

Swap Amounts

  • Input amount encrypted from user wallet
  • Output amount encrypted to recipient
  • No one can see trading sizes

Pool Reserves

  • Stored as euint64 ciphertexts
  • Only pool contract can compute with them
  • Observers cannot determine liquidity depth

Price Impact

  • Cannot be calculated without reserves
  • Front-runners cannot predict slippage
  • MEV bots cannot sandwich attacks

Trading History

  • Individual swap amounts not revealed
  • Cannot track user trading patterns
  • Volume analytics require hooks

What Is Public

Swap Events

  • That a swap occurred (yes/no)
  • Direction (A→B or B→A)
  • Sender and recipient addresses
  • Request IDs and settlement status

Pool Existence

  • Token pair addresses
  • Swap fee percentage
  • Owner address
  • Initialization status

**Not revealed:**Actual amounts, reserves, or pricing ratios


Security Considerations

Oracle Trust Model

The fhEVM oracle is a threshold network:

  • Multiple validators hold key shares
  • Require threshold (e.g., 7 of 10) to decrypt
  • No single validator can decrypt alone
  • Cryptographic proof of valid decryption

Reentrancy Protection

All swap functions use nonReentrant modifier:

  • Prevents reentrancy attacks
  • Safe token transfers
  • No recursive settlement

Front-Running Resistance

Traditional AMMs suffer from:

  • Sandwich attacks (front-run + back-run)
  • JIT (Just-In-Time) liquidity attacks
  • MEV extraction

Lunarys prevents this because:

  • Swap amounts encrypted
  • Cannot calculate price impact
  • Cannot predict profits
  • Economic security through encryption

Gas Costs

OperationEstimated GasNotes
swapExactAForB~600,000FHE operations expensive
settleSwap~400,000Division and transfers
Total per swap~1,000,000Split across two transactions

Why high gas?

  • FHE operations (mul, div) cost 10-100x normal operations
  • Encrypted transfers more expensive
  • Worth it for privacy!

Example: Complete Swap Flow

User Perspective

// 1. Create encrypted input
const input = await fhevm.createEncryptedInput(poolAddress, userAddress);
input.add64(ethers.parseUnits("100", 6)); // Swap 100 tokens
const encrypted = await input.encrypt();

// 2. Approve pool to spend tokens (one-time)
await tokenA.setOperator(poolAddress, futureTimestamp);

// 3. Initiate swap
const tx = await pool.swapExactAForB(
encrypted.handles[0],
encrypted.inputProof,
userAddress, // recipient
"0x" // no hook context
);
const receipt = await tx.wait();

// 4. Extract request ID
const event = receipt.logs.find(log =>
pool.interface.parseLog(log)?.name === "SwapDecryptionRequested"
);
const requestID = event.args.requestID;

console.log(`Swap initiated. Request ID: ${requestID}`);
console.log("Waiting for oracle to settle...");

// 5. Wait for settlement (automatic by oracle)
// Listen for SwapSettled event
pool.once(pool.filters.SwapSettled(requestID), (
reqId, sender, recipient, aForB, amountOut
) => {
console.log("Swap completed!");
console.log(`Output amount (encrypted): ${amountOut}`);
});

Oracle Perspective

// Oracle monitors for SwapDecryptionRequested events
pool.on("SwapDecryptionRequested", async (requestID, sender, recipient, aForB, handle) => {
// 1. Threshold validators decrypt the masked denominator
const decrypted = await thresholdNetwork.decrypt(handle);

// 2. Validators sign the decrypted value
const proof = await thresholdNetwork.generateProof(requestID, decrypted);

// 3. One validator submits settlement
const cleartexts = abi.encode(["uint256", "uint256"], [decrypted, liquidityConstant]);
await pool.settleSwap(requestID, cleartexts, proof);
});


Summary

The Lunarys swap mechanism achieves true confidentiality through:

  1. Fully encrypted state - All reserves and amounts encrypted
  2. FHE computations - AMM math on encrypted values
  3. Masked decryption - Privacy-preserving oracle interaction
  4. Two-phase design - Initiate → Oracle → Settle
  5. Front-run resistance - MEV cannot extract value

This is the first confidential constant product AMM with provable privacy guarantees!