Skip to content

Join

Initialize a pool with starting liquidity. Join is a mutating dispatch primitive — it routes to the protocol-specific mint operation underneath the unified interface. See The Primitive Contract for the cross-cutting invariants every dispatcher follows.

The apply() signature varies by protocol because the meaning of pool initialization varies by protocol. The first two parameters are common across all four; the trailing parameters are protocol-specific.

ProtocolRequired call shape
Uniswap V2Join().apply(lp, user_nm, amount0, amount1)
Uniswap V3Join().apply(lp, user_nm, amount0, amount1, lwr_tick, upr_tick)
BalancerJoin().apply(lp, user_nm, pool_shares)
StableswapJoin().apply(lp, user_nm, ampl_coeff)
ParameterTypeDescription
lpExchangePool object returned by factory.deploy(...). Must be empty (no prior Join).
user_nmstrAccount name receiving the LP credit for this initialization.

The remaining parameters depend on what initialization means for the protocol — see the per-protocol sections below.

Two-sided deposit at the pool’s natural price. The two amounts you pass define the initial price ratio; subsequent traders observe price = amount1 / amount0 based on what you seeded.

ParameterTypeNotes
amount0floatAmount of tkn0 (the first token from UniswapExchangeData).
amount1floatAmount of tkn1. The ratio amount1 / amount0 becomes the initial price.
from defipy import (
ERC20, UniswapExchangeData, UniswapFactory, Join,
)
eth = ERC20("ETH", "0x09")
tkn = ERC20("TKN", "0x111")
exch_data = UniswapExchangeData(
tkn0=eth, tkn1=tkn, symbol="LP", address="0x011",
)
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exch_data)
Join().apply(lp, "user", 1000, 100000)
# Initial price: 100 TKN per ETH

Initial liquidity is √(amount0 · amount1). V2 also burns a small MINIMUM_LIQUIDITY to a sentinel address on first mint to prevent share-price manipulation — treat that sentinel as out-of-band when counting LPs.

Two-sided deposit into a specific tick range. Concentrated liquidity has no meaning without a range, so V3 requires lwr_tick and upr_tick in addition to the two amounts. For a position equivalent to V2’s full-range behavior, use UniV3Utils.getMinTick(tick_spacing) and getMaxTick(tick_spacing).

ParameterTypeNotes
amount0floatAmount of tkn0.
amount1floatAmount of tkn1. Together with amount0, sets the initial sqrt-price via encodePriceSqrt(amount1, amount0).
lwr_tickintLower tick of the range. Must be a multiple of the pool’s tick_spacing.
upr_tickintUpper tick of the range. Must be > lwr_tick and a multiple of tick_spacing.
from defipy import (
ERC20, UniswapExchangeData, UniswapFactory, Join, UniV3Utils,
)
eth = ERC20("ETH", "0x09")
tkn = ERC20("TKN", "0x111")
tick_spacing = 60
fee = 3000
exch_data = UniswapExchangeData(
tkn0=eth, tkn1=tkn, symbol="LP", address="0x011",
version="V3", tick_spacing=tick_spacing, fee=fee,
)
factory = UniswapFactory("ETH pool factory", "0x2")
lp = factory.deploy(exch_data)
lwr_tick = UniV3Utils.getMinTick(tick_spacing)
upr_tick = UniV3Utils.getMaxTick(tick_spacing)
Join().apply(lp, "user", 1000, 100000, lwr_tick, upr_tick)
# Full-range V3 position; behaves analogously to V2 within the range

Pool-shares-based initialization. Token amounts are already loaded into the vault before Join is called — Join issues the initial pool-share supply against those vault balances. The single positional argument is the number of pool shares to mint, not a token amount.

ParameterTypeNotes
pool_sharesfloatInitial pool-share supply minted to user_nm. The vault balances divided by pool_shares define the per-share token claim.
from defipy import (
ERC20, BalancerVault, BalancerExchangeData, BalancerFactory, Join,
)
dai = ERC20("DAI", "0x111")
usdc = ERC20("USDC", "0x999")
dai.deposit(None, 10000)
usdc.deposit(None, 20000)
vault = BalancerVault()
vault.add_token(dai, 10) # denormalized weight
vault.add_token(usdc, 40) # denormalized weight
exch_data = BalancerExchangeData(vault=vault, symbol="BSP", address="0x3")
factory = BalancerFactory("pool factory", "0x2")
lp = factory.deploy(exch_data)
Join().apply(lp, "user", 100)
# 100 pool shares minted to "user"; per-share claim is the vault contents / 100

Amplification-based initialization. Like Balancer, token amounts are loaded into the vault beforehand. The signature parameter is named shares at the dispatcher level, but it’s passed straight to lp.join_pool(vault, ampl_coeff, to) — so semantically the third positional argument is the amplification coefficient A. Real-world tests and notebooks call it AMPL or AMPL_COEFF.

ParameterTypeNotes
ampl_coeffintStableswap amplification coefficient. Higher A = flatter invariant near peg = lower slippage on small trades but sharper depeg risk past the basin. Typical values: 10 (cautious), 100 (Curve 3pool-class), 2000 (high-amplification stable basket).
from defipy import (
ERC20, StableswapVault, StableswapExchangeData,
StableswapFactory, Join,
)
dai = ERC20("DAI", "0x111", 18)
usdc = ERC20("USDC", "0x222", 6)
dai.deposit(None, 10000)
usdc.deposit(None, 20000)
vault = StableswapVault()
vault.add_token(dai)
vault.add_token(usdc)
exch_data = StableswapExchangeData(vault=vault, symbol="LP", address="0x011")
factory = StableswapFactory("Stableswap factory", "0x2")
lp = factory.deploy(exch_data)
AMPL_COEFF = 2000
Join().apply(lp, "user", AMPL_COEFF)
# Pool initialized at A=2000; LP supply derived from vault balances + invariant

How Join interacts with the rest of the pipeline

Section titled “How Join interacts with the rest of the pipeline”

After Join, the typical pipeline is:

  1. Join — initialize the pool (this primitive).
  2. Swap — exercise the pool with trades to accrue fees / move price.
  3. Analytics (Agentic Primitives) — AnalyzePosition, SimulatePriceMove, CheckPoolHealth, etc. read the pool’s state.
  4. AddLiquidity / RemoveLiquidity — additional LPs join or exit.
  5. SwapDeposit / WithdrawSwap (V2/V3 only) — single-sided deposit/withdrawal flows.