Skip to main content

Liquidity Provision

Overview

Lunarys uses a Uniswap V2-style liquidity model with encrypted reserves. Liquidity providers deposit both tokens symmetrically and receive encrypted LP shares (CERC20 tokens). The pool uses a two-step process: bootstrap (one-time initialization) followed by contribute (ongoing deposits).

Liquidity Lifecycle

flowchart LR
subgraph Bootstrap["Bootstrap (Owner, One-Time)"]
B1[Deposit Token A + Token B] --> B2["Mint LP = (amtA >> 1) + (amtB >> 1)"]
end

subgraph Contribute["Contribute (Any User)"]
C1[Fetch & decrypt obfuscated state] --> C2[Encrypt deposit amounts]
C2 --> C3[Call contributeLiquidity]
C3 --> C4["Mint LP = min(ratioA, ratioB)"]
end

subgraph Remove["Remove Liquidity"]
R1[Encrypt LP shares to burn] --> R2[Call removeLiquidity]
R2 --> R3["Proportional withdrawal − 0.05% fee"]
end

Bootstrap --> Contribute
Contribute --> Remove

style Bootstrap fill:#2d3748,color:#fff
style Contribute fill:#2d3748,color:#fff
style Remove fill:#2d3748,color:#fff

Bootstrap (One-Time Initialization)

Function Signature

function bootstrap(
externalEuint64 amountAExt,
externalEuint64 amountBExt,
bytes calldata inputProof
) external

Flow

  1. Only callable by pool owner
  2. Can only be called once (reservesInitialized flag)
  3. Pulls encrypted amounts of both tokens from caller
  4. Mints LP tokens: liquidityAmount = (amountA >> 1) + (amountB >> 1)
  5. Sets reservesInitialized = true
  6. Generates initial obfuscated state

Events

event LiquiditySeeded(address indexed provider);

Contribute Liquidity (After Bootstrap)

Function Signature

function contributeLiquidity(
externalEuint64 amountAExt, // Encrypted amount of token A
externalEuint64 amountBExt, // Encrypted amount of token B
bytes calldata amountProof, // FHE proof for amounts
uint128 decryptedORA, // Decrypted obfuscated reserve A
uint128 decryptedORB, // Decrypted obfuscated reserve B
uint128 decryptedOL, // Decrypted obfuscated LP supply
bytes calldata OProof // FHE proof for obfuscated state
) external

Flow

  1. Validate obfuscated state proof against current ciphertexts
  2. Pull both token amounts from user
  3. Compute LP tokens to mint (proportional, factor cancels):
    mintA = amountA * obfTotalSupply / obfReserveA
    mintB = amountB * obfTotalSupply / obfReserveB
    mintAmount = min(mintA, mintB)
  4. Mint LP tokens to caller
  5. Update reserves and rotate obfuscation factor

Why Factor Cancels

The obfuscation factor f appears in both numerator and denominator:

amountA * (totalSupply * f) / (reserveA * f) = amountA * totalSupply / reserveA

So proportional minting works correctly with obfuscated values.

Excess Deposits

If a user provides non-proportional amounts, only the minimum ratio is minted. The excess stays in the pool as an implicit donation to all LPs.

Remove Liquidity

Function Signature

function removeLiquidity(
externalEuint64 sharesToRemoveExt, // Encrypted LP shares to burn
bytes calldata sharesProof, // FHE proof
uint128 decryptedORA, // Decrypted obfuscated reserve A
uint128 decryptedORB, // Decrypted obfuscated reserve B
uint128 decryptedOL, // Decrypted obfuscated LP supply
bytes calldata OProof // FHE proof for obfuscated state
) external

Flow

  1. Validate proof
  2. Transfer LP tokens into pool (burns source)
  3. Compute proportional withdrawal:
    grossA = sharesToRemove * obfReserveA / obfTotalSupply
    grossB = sharesToRemove * obfReserveB / obfTotalSupply
  4. Apply 0.05% withdrawal fee:
    feeA = grossA * 500 / 1,000,000
    feeB = grossB * 500 / 1,000,000
    netA = grossA - feeA
    netB = grossB - feeB
  5. Transfer net amounts to user
  6. Burn LP tokens
  7. Update reserves and rotate obfuscation factor

Withdrawal Fee

A 0.05% (500 BPS) fee is applied on liquidity removal to prevent Just-In-Time (JIT) liquidity attacks. The fee stays in the pool, benefiting remaining LPs.

Events

event LiquidityAdjusted(address indexed caller, bool add);

LP Token Model

CERC20 LP Shares

The EPOOL contract itself is a CERC20 token -- LP shares are encrypted and tracked on-chain:

  • Encrypted balances: Only the LP can see their own share count
  • 6 decimals: Consistent with underlying tokens
  • No public tracking: No fee accumulators or public growth metrics

Fee Realization

LPs realize accumulated swap fees when they remove liquidity. Since swap fees are added to reserves (Uniswap V2 style), the pool's reserve-to-supply ratio grows over time. When an LP removes their shares, they receive proportionally more tokens than they deposited.

Prerequisites

Before adding liquidity, users must:

  1. Set operator on both tokens: token.setOperator(poolAddress, expiryTimestamp)
  2. Fetch and decrypt current obfuscated state via the FHE gateway
  3. Encrypt deposit amounts via FHE SDK

Security Considerations

  • Proof validation: Obfuscated state proof prevents manipulation
  • Proportional minting: min(ratioA, ratioB) prevents dilution attacks
  • Withdrawal fee: Prevents JIT liquidity exploitation
  • Reentrancy protection: All functions use ReentrancyGuard
  • One-time bootstrap: Prevents reinitialization