Skip to main content
Version: 1.2.0

Liquidity

Module name and store key: liquidity. Consensus version 5.

x/liquidity is a constant-product automated market maker. It holds two-denom pools, mints LP shares through tokenfactory, lets LPs stake those shares to earn a share of swap fees and a governance-funded emission, and governs the whole pool lifecycle: pause, fee changes, ownership transfer, closure, and retirement.

Pools​

A pool is identified by a uint64 id and holds two denoms in canonical lexicographic order, so denom0 < denom1 and a pair always resolves to the same pool.

FieldMeaning
pool_id, ownerIdentity and administrative owner.
denom0, denom1The pair, sorted.
reserve0, reserve1Reserves, mirrored in the liquidity-reserve-{id} module account.
total_sharesLP supply, mirrored in the module's mint and burn rights.
lp_share_denomAlways factory/liquidity/pool/{pool_id}.
swap_fee_ppm, lp_fee_share_ppm, staking_fee_share_ppmFee level and its split. Parts per million, scale 1,000,000.
paused, emergency_pausedOwner-level and governance-level stop.
closingSet by governance; the pool only accepts the final withdrawal.
farm_lock_duration_blocksMinimum staking period for LP shares.
cumulative_lp_fees0, cumulative_lp_fees1Running LP fee counters.
pending_*Queued fee and ownership changes with their activation heights.

Three module accounts back a pool, all created on demand with no special permissions: liquidity-reserve-{id}, liquidity-farm-{id}, and liquidity-fee-{id}.

The math​

Every number is integer, and every division floors. The fee scale is 1,000,000.

Initial shares are the geometric mean of the two initial amounts:

shares = floor(sqrt(amount0 * amount1))

The result must be at least min_initial_liquidity, which is 2 by default.

Adding liquidity credits the smaller of the two proportional shares and returns the unused remainder:

shares0 = floor(amount0 * totalShares / reserve0)
shares1 = floor(amount1 * totalShares / reserve1)
shares = min(shares0, shares1)
used0 = floor(shares * reserve0 / totalShares)
used1 = floor(shares * reserve1 / totalShares)

Removing liquidity pays the proportional share of each reserve:

amount_i = floor(shares * reserve_i / totalShares)

Swapping in nets the fee off the top and applies the constant-product rule:

fee = floor(amountIn * swapFeePPM / 1_000_000)
netIn = amountIn - fee
amountOut = floor(reserveOut * netIn / (reserveIn + netIn))

netIn must be positive, so a swap too small to cover its own fee is rejected rather than rounded to zero.

Swapping out cannot simply be inverted, because fee collection floors and the algebraic inverse is then only an upper bound. The implementation takes a ceiling bound and binary-searches for the smallest gross input whose net value still covers the reserve increase.

Fee split. On every swap:

totalFee = floor(amountIn * swapFeePPM / 1_000_000)
stakingFee = floor(totalFee * stakingFeeSharePPM / 1_000_000)
lpFees += totalFee - stakingFee

The staking fee leaves the reserve, moves to liquidity-fee-{id}, and is split across stakers in proportion to their staked shares. The rounding remainder goes to the lexicographically smallest owner address, so the split is exact and deterministic. When a pool has no staked shares, the whole fee stays in the LP counter.

:::note Cumulative LP fees are a counter only cumulative_lp_fees0 and cumulative_lp_fees1 accumulate, but the current code has no message that pays them out. They are observable, not withdrawable. :::

Farming​

Staking LP shares earns a pro-rata share of the pool's staking fee plus any emission schedule the owner funds.

A farm position is a per-(pool, owner) amount plus a list of lots, one per deposit, each recording its deposit and unlock height.

RuleBehaviour
DepositAdds a lot with unlock_height = height + farm_lock_duration_blocks.
WithdrawStrict FIFO. If any lot is still locked, the whole withdrawal is rejected.
Emergency withdrawalGovernance-triggered paths skip the lock check and forfeit the accrued rewards.
ClaimPays both the accrued fees and the accrued emission, and clears the record.

The pool must not be paused, the LP denom must be poolable, and the position and lot counts are pre-flighted against their caps before any LP is transferred.

Emission schedules​

A schedule is a governance-approved stream of ucen from the pool owner to stakers, bounded by block heights.

budget, distributed, start_height, end_height, last_height
pending_end_height, pending_budget, pending_activation_height

At BeginBlock each schedule settles:

rate = budget / (end_height - start_height)
amount = min(rate * (end_height - last_height), budget - distributed)

Each staker receives floor(amount * shares / stakedShares). Whatever the per-user floor leaves behind stays in the schedule and remains refundable. Changes are queued: an update takes effect after emission_cooldown_blocks and extends the end height, and a refund is only allowed once the schedule has reached its end height.

Pool lifecycle​

TransitionWhoDelay
Pause / unpausePool ownerImmediate
Emergency pause / unpauseModule authorityImmediate
Fee changePool ownerfee_update_cooldown_blocks
Ownership transferCurrent owner, then the nomineeownership_cooldown_blocks
Emergency ownership recoveryModule authorityemergency_ownership_cooldown_blocks
ClosureModule authorityImmediate; sets closing and clears all pending state
RetirementAny holderOnly by withdrawing every share

All cooldowns fall back to 720 blocks when the parameter is zero, and none may exceed 2,628,000 blocks.

The owner cannot cancel a pending emergency ownership transfer, and the module authority cannot initiate a normal one. The two paths are separate on purpose.

Messages​

MessageSignerConstraint
MsgCreatePoolsenderPool creation enabled, fee parameters in range, both assets poolable, no LP denoms, minimum initial liquidity met.
MsgAddLiquiditysenderOptional min_shares slippage floor.
MsgRemoveLiquiditysendermin_amount0 and min_amount1 slippage floors. Withdrawing every share closes the pool.
MsgSwapExactAmountInsenderOptional token_out_min_amount slippage floor.
MsgSwapExactAmountOutsendertoken_in_max_amount is required.
MsgDepositFarm / MsgWithdrawFarm / MsgClaimFarmsenderSee farming.
MsgCreateEmissionSchedule / MsgUpdateEmissionSchedule / MsgRefundEmissionSchedulesenderSchedule owner.
MsgRequestFeeUpdate / MsgCancelFeeUpdatesenderPool owner, with the split summing to the full fee scale.
MsgRequestOwnershipTransfer / MsgAcceptOwnershipTransfer / MsgCancelOwnershipTransfersenderCurrent owner, then the nominee.
MsgRequestEmergencyOwnershipTransfer / MsgActivateEmergencyOwnershipTransferauthorityModule authority.
MsgPausePool / MsgUnpausePoolsenderPool owner.
MsgEmergencyPausePool / MsgEmergencyUnpausePoolauthorityModule authority.
MsgClosePoolauthorityModule authority. Idempotent.
MsgRecoverPoolCoinssenderPool owner, one of the three accounts, and only for denoms that are not registered IBC denoms.
MsgRegisterIBCDenom / MsgRemoveIBCDenomauthorityModule authority. The denom must be ibc/{sha256(full_trace)} with a consistent trace.
MsgUpdateParamsauthorityModule authority.

The IBC denom registry is a governance allow-list: registering a denom marks it approved and records its port, channel, and base denom, and it gates risk-increasing operations on that asset.

Params​

FieldDefault
pool_creation_enabledtrue
min_swap_fee_ppm / max_swap_fee_ppm100 / 10,000
fee_scale1,000,000 (must equal this exactly)
emission_cooldown_blocks720
ownership_cooldown_blocks720
fee_update_cooldown_blocks720
emergency_ownership_cooldown_blocks720
min_lp_fee_share_ppm / max_lp_fee_share_ppm0 / 1,000,000
min_initial_liquidity2
max_pools_per_owner0, meaning unlimited

Per-pool defaults when a message leaves the field at zero: swap fee 3,000 ppm, LP share 300,000 ppm, staking share 700,000 ppm.

Queries​

QueryReturns
ParamsThe module parameters.
Pool, PoolByPair, PoolsOne pool, the pool for a denom pair, or a paginated list.
PoolSharesTotal, circulating, locked, and farm-staked shares.
FarmPositionA position's amount, claimable fee rewards, and claimable emission.
EmissionScheduleA schedule and its remaining budget.
PendingUpdatesEvery queued fee and ownership change with its activation height.
PoolAccountBalancesThe three module account balances.
SimulateSwapExactAmountIn / SimulateSwapExactAmountOutA quote without touching state, using the fee that will actually be active.
IBCDenom / IBCDenomsThe IBC registration allow-list.

Queries read a private copy of the state, so a query can never write.

:::note No APY query There is no annualised or APY query in the current code. Fee and emission accrual are available per position through FarmPosition, and the running totals are visible in Pool. :::

Invariants and limits​

BeginBlock loads the state, activates due fee updates, and settles every emission schedule. EndBlock runs the in-memory invariants on every block, and reconciles against bank balances and supply every 10,000 blocks when the bank exposes those readers.

A violation emits liquidity_invariant_violation and returns no error, so block finalization is never interrupted by an alarm.

LimitValue
Pools10,000
Genesis records100,000
Farm positions / per pool10,000 / 1,000
Farm lots / per position100,000 / 10,000
Farm reward records / per pool10,000 / 1,000
Emission reward records / per pool10,000 / 1,000
Reward denoms per record64
Farm lock duration2,628,000 blocks
Any cooldown2,628,000 blocks

Every state amount is bounded at 2^256 − 1. Limits are checked before any transfer: if a new fee denom would push a record past its cap, the swap is rejected and accruedFees is left untouched.

Events​

EventAttributes
liquidity_add, liquidity_removepool_id, sender, shares, used0/amount0, used1/amount1
swap_exact_in, swap_exact_outpool_id, denom_in, amount_in, denom_out, amount_out
farm_deposit, farm_withdraw, farm_claimpool_id, owner, amount, claimed
fee_update_pending, fee_update_activated, fee_update_cancelledpool_id and the fee triple
emission_schedule_created, emission_schedule_update_pending, emission_schedule_refundedpool_id
ownership_transfer_requested, ownership_transferred, ownership_transfer_cancelledpool_id, owner, new_owner
emergency_ownership_recovery_requested, emergency_ownership_recoveredpool_id, owner, new_owner
pool_closing, pool_retiredpool_id
pool_emergency_pause, pool_emergency_unpausepool_id
pool_coins_recoveredpool_id, owner, account_type, denom, amount
liquidity_invariant_violationheight, error

Errors​

CodeIdentifierRaised when
1100ErrInvalidPoolPool state or parameters are inconsistent.
1101ErrInvalidParamsA parameter is outside its allowed range.
1102ErrInvalidGenesisThe genesis state is inconsistent.
1103ErrInvalidAmountAn amount is nil, zero, negative, or out of bounds.
1104ErrInsufficientOutputA slippage bound was violated.
1105ErrInvalidFeeThe fee or fee split is invalid.
1106ErrOwnershipTransferA transfer is not allowed at this time.
1107ErrRecoveryA coin recovery is not permitted.
1108ErrLPSharePolicyThe LP denom does not satisfy the tokenfactory policy.

Aggregate record limits and fee record caps are reported as plain errors rather than registered errors.

CLI​

x/liquidity exposes its queries through autocli under the liquidity prefix, and its Msg service as centerchaind tx liquidity <msg>. Run centerchaind tx liquidity --help for the generated message commands.

centerchaind query liquidity params
centerchaind query liquidity pool [pool-id]
centerchaind query liquidity pool-by-pair [denom-a] [denom-b]
centerchaind query liquidity pools
centerchaind query liquidity pool-shares [pool-id]
centerchaind query liquidity farm-position [pool-id] [owner]
centerchaind query liquidity emission-schedule [pool-id]
centerchaind query liquidity pending-updates [pool-id]
centerchaind query liquidity pool-account-balances [pool-id]
centerchaind query liquidity simulate-swap-exact-in
centerchaind query liquidity simulate-swap-exact-out
centerchaind query liquidity ibc-denom [denom]
centerchaind query liquidity ibc-denoms

The two simulate queries are POST endpoints on the gRPC gateway; every other query is served as GET /centerchain/liquidity/v1/....