AMM pools

Tapp's Automated Market Maker (AMM) implements a classic constant product pool similar to Uniswap v2. Each pool holds two fungible assets and issues pool shares that track a provider's proportional ownership of those reserves. The logic lives in the amm::amm Move module.

This document explains both the Move code and the high‑level economics behind the AMM so liquidity providers (LPs) understand how trades affect the pool, how fees accrue and the risks involved.

Key data types

Config

  • authority: Address allowed to manage global settings.

  • pending_authority: Temporary storage while transferring authority.

  • fee_tiers: Supported trading fees expressed in parts per million (FEE_DENOMINATOR = 1_000_000).

Pool

A pool is represented by the Pool struct:

struct Pool has key, store {
    pool_addr: address,       // address holding the pool resources
    asset_a: address,         // coin type of token A
    asset_b: address,         // coin type of token B
    fee: u64,                 // fee in ppm
    reserve_a: u64,           // amount of token A in the pool
    reserve_b: u64,           // amount of token B in the pool
    total_shares: u128,       // total liquidity tokens minted
    positions: BigOrderedMap<u64, Position>,
    position_index: u64,
}

Position

When users add liquidity they receive a Position record which tracks the number of shares owned. A pool can have many positions and users may hold several of them.

Creating a pool

Pools are created with create_pool(pool_signer, assets, fee, creator).

  • pool_signer is the signer of the newly created account that will store the Pool resource.

  • assets is a vector with two addresses representing the coin types of token A and token B.

  • fee must match one of the configured fee tiers (0.01%, 0.05%, 0.3% or 1%).

  • creator is recorded in the PoolCreated event.

The function initializes reserves to zero and sets total_shares to zero. No liquidity exists until someone calls add_liquidity.

Adding liquidity

add_liquidity(pool_signer, position_idx, stream, creator) accepts a BCSStream that encodes the following arguments:

  1. amount_a – desired amount of token A to deposit

  2. amount_b – desired amount of token B

  3. min_amount_a – minimum A that must be deposited

  4. min_amount_b – minimum B that must be deposited

If position_idx is some, the existing position is topped up. Otherwise a new position is created.

The function calculates the optimal deposit amounts using calc_optimal_lp_amount, mints shares and updates reserves. The first liquidity provider permanently locks MINIMUM_LIQUIDITY (1000 shares) to protect against division by zero attacks. A LiquidityAdded event is emitted.

Returned values are:

  • A vector containing the final amounts of A and B deposited.

  • some(index) if a new position was created; otherwise none.

Removing liquidity

remove_liquidity(pool_signer, position_idx, stream, creator) burns shares and returns the underlying tokens. The stream encodes:

  1. burned_shares – amount of liquidity tokens to burn

  2. amount_a_min – minimum A expected

  3. amount_b_min – minimum B expected

The amounts withdrawn are computed proportionally to the pool reserves using compute_lp_from_shares. If the resulting amounts are below the minimum thresholds the transaction aborts. If a position’s remaining share balance is zero it is removed from the map. A LiquidityRemoved event logs the operation.

Swapping tokens

swap(pool_signer, stream, creator) performs trades in either direction. The stream encodes:

  1. a2btrue to swap token A for token B; false for B → A

  2. fixed_amount_intrue if the provided amount is the input; false when specifying the desired output

  3. amount_in – amount of tokens sent (if fixed_amount_in)

  4. amount_out – minimum amount expected (when fixed_amount_in) or the exact amount requested (when swapping for a fixed output)

Depending on fixed_amount_in the module routes to swap_exact_tokens_for_tokens or swap_tokens_for_exact_tokens. In either case the pool uses the constant product model to determine the price impact. A Swapped event records the trade.

At any moment the marginal price of token A relative to B is simplyreserve_b / reserve_a. Trading against the pool shifts this ratio: buying A reduces its reserve and therefore raises its price, while selling A increases its reserve and lowers the price. This automatic price discovery lets the AMM function without relying on order books.

Constant product formula

Each pool maintains the invariant x * y = k where x and y are the reserves of token A and B. When a trade occurs the product of the reserves after fees must be at least as large as before.

For an input amount dx, reserves x0 and y0, and fee f (ppm):

amount_out = (y0 * dx * (1 - f/FEE_DENOMINATOR)) / (x0 + dx * (1 - f/FEE_DENOMINATOR))

Where:

  • x0, y0 – reserves of token A and B before the swap

  • dx – the amount of token A being traded in

  • f – the fee applied to the swap (parts per million)

  • FEE_DENOMINATOR – constant 1,000,000 used to express f

  • k – the constant product x * y that remains after the swap

The result amount_out is the amount of token B returned while keeping the constant product k = x * y from decreasing after fees, where x and y are the new reserves after the swap.

This equation keeps the pool on the same constant product curve. The inverse formula computes the input needed for a specific output.

The spot price of token A in terms of B is the ratio reserve_b / reserve_a. As trades move this ratio the price adjusts automatically.

Fees and LP incentives

The fee f is deducted from every trade and added to the reserves. Because liquidity providers own a share of the reserves, these fees accumulate proportionally to their holdings. Over time LPs earn yield from trading volume.

A simple approximation for APR is:

APR ≈ (daily volume × fee × 365) / total_liquidity

Pools with high volume relative to their liquidity tend to generate more fees. However, volatile assets can experience larger price swings which lead to impermanent loss as described below.

Impermanent loss

Impermanent loss (IL) is the difference between holding tokens in the pool versus simply holding them in a wallet. If the price of either asset moves significantly, the value of your pool position may end up lower than if you had held the tokens outright. IL is "impermanent" because it may diminish if prices return to their original ratio. Fees earned can offset IL, especially for stable pairs with large trading volume.

Utility functions

  • fee_rate(address) – fetch the fee tier of a pool.

  • reserve_a, reserve_b – current reserves.

  • total_shares – total minted liquidity shares.

  • position_shares / get_position – inspect individual liquidity positions.

These helpers can be used off‑chain to display pool info or compute prices.

Events

Every action emits an event so indexers can track pool activity:

  • PoolCreated

  • LiquidityAdded

  • LiquidityRemoved

  • Swapped

Security checks

The Move code performs numerous assertions such as ensuring supported fee tiers, verifying liquidity amounts, preventing arithmetic underflow, and checking that swap invariants hold. These checks are designed to safeguard liquidity providers and traders from invalid state transitions.


This AMM forms the foundation for Tapp’s more advanced pool types. Understanding its mechanics is essential for anyone integrating directly with the contracts or building hooks that extend the protocol.

Last updated