AMMs and Liquidity
Day 8··12 min read

LP Shares and Liquidity Accounting

Initial liquidity, proportional ownership, imbalanced deposits, withdrawals, fee accrual, and minimum-liquidity locking.

View the tested implementation

Days 6 and 7 treated the AMM as a trading machine. Day 8 asks who owns that machine—and how deposits, withdrawals, fees, and integer rounding change those ownership claims.

The complete Rust implementation and its 115 passing tests live in the DeFi AMM repository.

An LP share is not a fixed amount of either token. It is a proportional claim on both reserves together.

One share, two reserve claims

Suppose a pool contains:

x = 200 SOL
y = 30,000 USDC

If Alice owns all 1,000 outstanding LP shares, she owns 100% of both reserves. In general:

ownership = user LP shares / total LP shares

For a user holding s shares out of total supply S:

X claim = floor(s × x / S)
Y claim = floor(s × y / S)

This is Module 1 vault accounting applied to a basket of two assets. LP shares express ownership; the reserves determine what that ownership is currently worth.

A fair proportional deposit

Bob adds:

Delta x = 50 SOL · Delta y = 7,500 USDC

Both amounts equal 25% of the existing reserves:

50 / 200 = 7,500 / 30,000 = 25%

Against an illustrative supply of 1,000 LP shares, each asset supports the same mint:

shares from X = 50 × 1,000 / 200 = 250
shares from Y = 7,500 × 1,000 / 30,000 = 250

The pool becomes 250 SOL / 37,500 USDC with 1,250 total LP shares. Bob added 25% of the old pool but owns 20% of the new one:

250 / 1,250 = 20%

His immediate claims are exactly the assets he supplied. Alice still claims exactly 200 SOL and 30,000 USDC. No value moved between them.

The reserve ratio is also unchanged:

37,500 / 250 = 30,000 / 200 = 150

That gives the fairness invariant: a proportional deposit increases reserves and ownership claims together without repricing the pool.

Initial liquidity needs a different rule

The proportional mint formula needs existing reserves and an existing LP supply. It is undefined for an empty pool.

The first LP supply is instead based on the geometric mean:

L_0 = floor(sqrt(x_0 × y_0))

For 200 SOL and 30,000 USDC in human units:

sqrt(200 × 30,000) = sqrt(6,000,000) ≈ 2,449.4897

The geometric mean treats both reserves symmetrically. If both quantities double, initial liquidity doubles:

sqrt((2x)(2y)) = 2sqrt(xy)

The Uniswap v2 whitepaper uses this geometric-mean rule so the initial LP supply is independent of how either asset is numerically denominated. The Rust model works in base units and uses floor u128::isqrt.

With six decimal places on both example assets:

x_0 = 200,000,000 · y_0 = 30,000,000,000
L_0 = floor(sqrt(x_0y_0)) = 2,449,489,742

Permanently locking minimum liquidity

The model permanently locks a small part of the first LP supply:

provider liquidity = L_0 − minimum liquidity
total liquidity = provider liquidity + locked liquidity

The default lock is 1,000 LP base units, matching MINIMUM_LIQUIDITY in the official Uniswap v2 pair contract.

Those shares can never be redeemed. This prevents every user-owned share from disappearing while economically meaningful reserves remain, and raises the cost of manipulating LP share value through tiny initial supply and asset donations.

It is a defence, not magic. It does not eliminate every possible manipulation, and the exact economic protection depends on token decimals and pool scale.

Initialization therefore requires:

floor(sqrt(x_0y_0)) > minimum liquidity

Zero reserves, repeated initialization, overflow, and an initial mint too small to exceed the lock all fail atomically.

Later deposits use the limiting asset

For an initialized pool, a maximum deposit (Delta x, Delta y) supports two possible share amounts:

L_x = floor(Delta x × S / x)
L_y = floor(Delta y × S / y)

The fair mint is:

L_minted = min(L_x, L_y)

Consider 200 SOL, 30,000 USDC, and 1,000 LP shares. Bob offers 50 SOL but only 6,000 USDC:

L_x = 250 · L_y = 200

The USDC side is limiting, so Bob receives 200 shares. Giving him 250 would sell him ownership that his quote-asset contribution does not support.

The opposite imbalance has the same answer. If Bob offers 40 SOL and 7,500 USDC:

L_x = 200 · L_y = 250 · L_minted = 200

Only the proportional amount should enter the pool. Excess assets must be returned or explicitly reported—not silently donated as though they bought ownership.

Accepted amounts round up

Once the LP mint is known, the implementation reconstructs the token amounts required to back it:

X accepted = ceil(L_minted × x / S)
Y accepted = ceil(L_minted × y / S)

Required assets round up so the newly minted shares are never underfunded. LP shares themselves round down so the entrant is never over-issued ownership.

The quote reconciles each caller maximum exactly:

maximum X = accepted X + unused X
maximum Y = accepted Y + unused Y

This produced the day's most useful counterexample. After initializing the base-unit pool above, a seemingly exact 50-SOL / 7,500-USDC proportional deposit mints 612,372,435 LP units. It accepts:

  • 50,000,000 X units, with zero X unused.
  • 7,499,999,994 Y units, with 6 Y units unused.

The human ratio is exact, but floor(shares) followed by ceil(required tokens) is not necessarily a perfect inverse unless the total supply divides the products cleanly. The six-unit remainder is derived integer behaviour, not a failed proportionality check.

Removing liquidity

Burning b LP shares returns the same fraction of both reserves:

X out = floor(b × x / S)
Y out = floor(b × y / S)

Both outputs round down so the pool never overpays a withdrawing LP.

The state transition is:

x' = x − X out
y' = y − Y out
S' = S − b

Withdrawal limits protect the LP in both assets:

X out ≥ minimum X · Y out ≥ minimum Y

A limit exactly equal to the preview succeeds. A one-unit stricter limit fails before reserves or total supply change.

Why locked liquidity changes the final withdrawal

Redeemable supply excludes permanently locked LP units:

redeemable liquidity = total liquidity − locked liquidity

After every redeemable share exits the worked base-unit pool:

  • Total liquidity equals locked liquidity: exactly 1,000 units.
  • The pool retains 82 X base units.
  • The pool retains 12,248 Y base units.

No user can claim those residual reserves because no user owns the locked shares.

The tests also explored a zero-lock configuration. Burning the complete supply would send both reserves to zero, but ConstantProductPool intentionally cannot represent zero reserves. The wrapper rejects that transition as ImpossibleReserveTransition rather than constructing an invalid inner pool.

That is an architectural constraint stated honestly: this model supports permanent-liquidity AMMs, not complete pool closure.

Fees accrue without minting LP shares

Swaps change reserves but do not change LP supply:

S_after swap = S_before swap

Because the Day 6 input fee remains inside the reserves, existing LP percentages remain constant while the assets backing those percentages change.

This does not mean both token claims or externally marked portfolio value must rise after every swap. A swap changes reserve composition, and market prices move. The defensible accounting statement is narrower:

  • Fees remain in pool reserves.
  • Total LP supply is unchanged.
  • Existing ownership percentages are unchanged.

New liquidity must therefore be priced against the current post-swap reserves and current LP supply. Using the initial reserves would let a newcomer acquire previously accumulated value too cheaply.

Why a wrapper is the honest architecture

The original ConstantProductPool::new accepts populated reserves because Days 6 and 7 study swap and arbitrage mathematics independently of ownership.

Retroactively adding an LP supply to every such pool would fabricate an owner for those reserves. Day 8 instead introduces LiquidityPool:

pub struct LiquidityPool {
    pool: Option<ConstantProductPool>,
    fee_bps: u16,
    minimum_liquidity: u64,
    total_liquidity: u64,
    locked_liquidity: u64,
}

It begins genuinely uninitialized. Only initialize_liquidity creates the inner constant-product pool and its corresponding LP supply.

The wrapper exposes the inner pool for Day 6 swaps and Day 7 arbitrage. Those operations cannot reach back into the wrapper's LP counters, making “trades do not mint or burn LP shares” a structural property rather than duplicated bookkeeping.

Liquidity additions and removals rebuild the validated inner pool with its new reserves. The swap formula itself remains untouched.

Aggregate supply versus Solana balances

This educational model tracks aggregate total_liquidity and locked_liquidity, not a map of individual providers.

In a Solana deployment, the LP asset would naturally map to a token mint whose mint state records total supply, while provider ownership lives in token accounts. Minting increases supply and credits an account; burning requires the token-account owner or an approved delegate and reduces supply. Those mechanics are described in the official Solana token documentation.

The crate therefore validates aggregate redeemable supply but intentionally does not claim to authorize a particular user's burn. Token-account ownership, signatures, transfers, and CPIs remain outside this arithmetic model.

Atomic liquidity transitions

Every preview is pure. Every state-changing operation follows the same order:

calculate → validate arithmetic → validate amounts → validate limits → commit

Failed initialization, zero-share deposits, excessive burns, attempts to burn locked liquidity, impossible reserve transitions, overflow, and slippage violations leave the entire wrapper unchanged.

The central supply invariant is:

total liquidity = locked liquidity + redeemable liquidity

Individual claims, if calculated separately, may leave rounding dust:

sum of floored X claims ≤ reserve X
sum of floored Y claims ≤ reserve Y

Exact equality is not assumed where independent floors make it mathematically false.

What the tests cover

Day 8 adds 52 tests: 35 example tests and 17 generated properties. Combined with all 63 unmodified Day 6–7 tests, the crate now has 115 passing tests.

They verify:

  • Geometric-mean initialization and floor-square-root behaviour.
  • Default and configurable minimum-liquidity locking.
  • Proportional and imbalanced deposits.
  • Accepted/unused asset reconciliation.
  • Zero-share deposit rejection.
  • Proportional withdrawals and downward output rounding.
  • Locked-liquidity and impossible-transition protection.
  • Add/remove slippage bounds and atomic failure.
  • Swap and arbitrage isolation from LP supply.
  • Current-reserve pricing after fee-generating swaps.
  • Preview purity and preview/execution agreement.
  • Mixed lifecycle invariants and checked-overflow rejection.

Property tests use a minimum lock of one unit for many small generated pools, while dedicated examples exercise the conventional default of 1,000. This expands the meaningful generated state space without changing production defaults.

The complete suite passes at 2,000 generated cases across five repeated runs. No Day 6 or Day 7 test required modification.

What I learned

  • LP shares are proportional claims over both reserves, not fixed token amounts.
  • Initial supply uses the geometric mean because no prior LP price exists.
  • Permanently locked liquidity prevents the entire supply from becoming redeemable.
  • Later mints use the minimum ownership supported by either supplied asset.
  • Accepted amounts round up; issued shares and withdrawal outputs round down.
  • Even an exactly proportional human deposit can leave base-unit change.
  • Swap fees accrue through reserve changes while LP supply stays fixed.
  • New liquidity must enter against current reserves, not historical ones.
  • Aggregate LP accounting and user burn authorization are separate layers.
  • A valid invariant needs a derived rounding argument, not an assumed one-unit tolerance.

Day 9 turns this ownership model into economics: impermanent loss, divergence loss, fee income, mark-to-market comparison, and when providing liquidity beats simply holding both assets.

References