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
PendingSwapstruct 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:
- FHE allows computation on encrypted data
- But final token transfers require knowing exact amounts
- Decryption must be done securely via threshold oracle
- 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
| Parameter | Type | Description |
|---|---|---|
amountInExt | externalEuint64 | Encrypted input amount - Created by user's wallet using FHEVM SDK |
proofIn | bytes | Zero-knowledge proof - Proves user knows the encrypted value without revealing it |
recipient | address | Destination address - Where output tokens will be sent |
hookContext | bytes | Optional 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:
- Convert external encrypted input to internal FHE ciphertext
- Verify the zero-knowledge proof
- Call
confidentialTransferFromto pull tokens - 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,000feeMultiplier = 1,000,000 - 3,000 = 997,000amountAfterFee = (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
| Parameter | Type | Description |
|---|---|---|
requestID | uint256 | The pending swap ID to settle |
cleartexts | bytes | Decrypted values from the oracle |
decryptionProof | bytes | Cryptographic 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:
denominatorRaw= decryptedmaskedDenominatorliquidityRaw= 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 Xy= reserve of token Yk= 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 tokenreserveOut= encrypted reserve of output tokenamountIn= encrypted input amountfee= 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
euint64ciphertexts - 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
| Operation | Estimated Gas | Notes |
|---|---|---|
swapExactAForB | ~600,000 | FHE operations expensive |
settleSwap | ~400,000 | Division and transfers |
| Total per swap | ~1,000,000 | Split 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);
});
Related Documentation
- PrivacyPool Contract - Full contract reference
- Checkpoints System - Historical reserve tracking
- Hook System - Extensibility
- DAO System - Governance
Summary
The Lunarys swap mechanism achieves true confidentiality through:
- Fully encrypted state - All reserves and amounts encrypted
- FHE computations - AMM math on encrypted values
- Masked decryption - Privacy-preserving oracle interaction
- Two-phase design - Initiate → Oracle → Settle
- Front-run resistance - MEV cannot extract value
This is the first confidential constant product AMM with provable privacy guarantees!