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_signeris the signer of the newly created account that will store thePoolresource.assetsis a vector with two addresses representing the coin types of token A and token B.feemust match one of the configured fee tiers (0.01%, 0.05%, 0.3% or 1%).creatoris recorded in thePoolCreatedevent.
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:
amount_a– desired amount of token A to depositamount_b– desired amount of token Bmin_amount_a– minimum A that must be depositedmin_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; otherwisenone.
Removing liquidity
remove_liquidity(pool_signer, position_idx, stream, creator) burns shares and returns the underlying tokens. The stream encodes:
burned_shares– amount of liquidity tokens to burnamount_a_min– minimum A expectedamount_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:
a2b–trueto swap token A for token B;falsefor B → Afixed_amount_in–trueif the provided amount is the input;falsewhen specifying the desired outputamount_in– amount of tokens sent (iffixed_amount_in)amount_out– minimum amount expected (whenfixed_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 swapdx– the amount of token A being traded inf– the fee applied to the swap (parts per million)FEE_DENOMINATOR– constant 1,000,000 used to expressfk– the constant productx * ythat 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_liquidityPools 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:
PoolCreatedLiquidityAddedLiquidityRemovedSwapped
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