Stable swap pools

Tapp's stable swap pools are designed for assets that trade near a 1:1 price ratio—like two stablecoins or wrapped versions of the same token. They implement Curve-style math in the stable::stable module to achieve very low slippage.

This guide summarizes the Move code and explains how liquidity providers interact with these pools.

Key data types

Config

  • authority: address allowed to modify global settings.

  • pending_authority: temporary storage when transferring authority.

  • fee_tiers: supported trading fees (0.01% and 0.05%).

Pool

Each pool stores many fields:

struct Pool has key, store {
    pool_addr: address,            // resource account for the pool
    n_coins: u64,                  // number of assets (2‒8)
    rate_multipliers: vector<u256>,// decimals normalization factors
    stored_balances: vector<u256>, // pool reserves
    fee: u256,                     // trade fee
    offpeg_fee_multiplier: u256,   // extra fee when off the peg
    initial_a: u256,               // A parameter at start of ramp
    future_a: u256,                // target A parameter
    initial_a_time: u64,           // ramp start time
    future_a_time: u64,            // ramp end time
    positions: BigOrderedMap<u64, Position>,
    position_index: u64,
    total_shares: u256,
}

Position

A Position simply tracks a user's share count:

struct Position has copy, drop, store {
    index: u64,
    shares: u256,
}

Creating a pool

create_pool(pool_signer, assets, assets_decimals, fee, rest_args, creator) initializes a new resource account that holds the Pool struct. Arguments include:

  • assets: vector of coin type addresses.

  • assets_decimals: decimal places for each asset so reserves can be normalised.

  • fee: must match a configured tier.

  • rest_args: BCS-encoded amp (initial A factor) and offpeg_fee_multiplier.

  • creator: recorded in the PoolCreated event.

Most other entry functions accept a stream: &mut BCSStream parameter that encodes additional arguments in Binary Canonical Serialization (BCS) format. This keeps the signatures short while allowing complex parameter lists.

Balances start at zero and rate_multipliers adjust each token to 36 decimals. The amplification factor A controls the curvature of the swap function and can later be adjusted through ramping operations.

Adding liquidity

add_liquidity(pool_signer, position_idx, stream, receiver) accepts a BCSStream that encodes:

  1. amounts – vector of deposit amounts (one per coin in the pool)

  2. min_mint_amount – minimum LP tokens to mint

The function then:

  1. Computes the current invariant D using get_D_mem.

  2. Transfers tokens in and updates stored_balances.

  3. Recalculates D and applies fees on any imbalance using a dynamic fee formula.

  4. Mints pool shares proportional to the change in D.

If position_idx is some, the existing position receives the new shares. Otherwise a new index is created.

Removing liquidity

remove_liquidity(pool_signer, position_idx, stream, creator) also expects a BCSStream. The first byte selects the withdrawal mode:

  1. Single coin withdrawal – stream then carries burn_amount, i (coin index) and min_received.

  2. Imbalanced withdrawal – followed by a vector amounts and max_burn_amount.

  3. Proportional withdrawal – remaining bytes encode burn_amount and min_amounts for each coin.

  • ramp_a(stream) – stream contains future_a (u256) and future_a_time (u64) to gradually change the amplification factor.

  • set_new_fee(stream) – stream provides new_fee (u256) and new_offpeg_fee_multiplier (u256).

Swapping tokens

swap(pool_signer, stream, receiver) reads the sold token index i, bought token index j, input amount dx and minimum output min_dy. It calculates normalized balances (xp), calls internal_swap to solve the stable swap invariant and applies a dynamic fee based on how far the trade moves the pool off the peg. Reserves are updated and a Swapped event is emitted.

Pool operations

Administrative actions are routed through run_pool_op:

  • ramp_a(stream) – gradually change the amplification factor between initial_a and future_a over time.

  • stop_ramp_a() – freeze A at its current value.

  • set_new_fee(stream) – update the fee and off-peg multiplier.

These operations emit RampA, StopRampA and ApplyNewFee events respectively.

Math overview

Stable pools use an invariant D derived from Curve's formula that blends constant sum and constant product behavior. Key helpers include:

  • get_D(xp, amp) – iteratively solves for the invariant given normalized balances xp and amplification factor amp.

  • get_y(i, j, x, xp, amp, d) – given a new balance x for coin i, computes the resulting balance of coin j while keeping D constant.

  • amp() – returns the current amplification coefficient, linearly interpolating during a ramp.

  • internal_dynamic_fee(xpi, xpj, fee) – increases the fee when trading pushes the pool away from the peg.

These functions ensure swaps and liquidity changes respect the invariant while allowing very efficient trades around the equilibrium price.

Utility functions

The module exposes several read‑only helpers:

  • n_coins, rate_multipliers, stored_balances

  • initial_a, future_a, initial_a_time, future_a_time

  • fee_rate and fee_denominator

  • total_shares, get_position and position_shares

  • get_dx / get_dy to preview swap amounts

Events

Stable swap pools emit rich events for indexers:

  • PoolCreated

  • LiquidityAdded

  • LiquidityRemoved, LiquidityOneRemoved, LiquidityImbalanceRemoved

  • Swapped

  • RampA and StopRampA

  • ApplyNewFee

Security considerations

The code asserts valid fee tiers, checks invariant calculations, prevents position underflow and enforces ramping constraints. These safeguards help maintain pool integrity and protect LPs.


Stable swap pools complement Tapp's AMM and CLMM offerings by enabling efficient trading between pegged assets. Understanding the Move implementation allows integrators to estimate pricing, manage liquidity and monitor the health of each pool.

Last updated