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 thePool
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 thePoolCreated
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:
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
–true
to swap token A for token B;false
for B → Afixed_amount_in
–true
if the provided amount is the input;false
when 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 expressf
k
– the constant productx * 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