Providing Liquidity

⚠️

Liquidity provision involves risk. This section describes how to use software tools, but your trading strategies are solely up to you.

Penumbra's DEX is built around order-book-style concentrated liquidity. Liquidity positions are constant-sum AMMs, offering to trade between assets at a fixed price. Each position is its own independent AMM with its own fee tier, and as described in the DEX chapter, the DEX engine indexes active liquidity positions and sorts them by price, then traverses the liquidity graph to optimally fill trades.

Liquidity positions are public, but are created out of and withdrawn back to Penumbra's shielded pool. Thus, while everyone can see the aggregate state of the market, they cannot see which positions are controlled by which accounts, or view an account's trading history or strategy (unless they have access to that account's viewing key).

Liquidity Positions

The data of a liquidity position is contained in the Position message (opens in a new tab). At a high level, this consists of:

  • the position's trading function, which specifies:
    • the trading pair (order-independent, as assets 1 and 2 are ordered by asset ID);
    • the trading function, via parameters p and q;
    • the percentage fee applied to trades;
  • the position's reserves, R_1 and R_2;
  • a close_on_fill boolean, controlling whether the position is closed when a trade completely consumes either R_1 or R_2.

The trading function for the CFMM is:

phi(R_1, R_2) = p * R_1 + q * R_2

A trade is accepted when phi(R_1, R_2) = phi(R_1', R_2'), so the ratio of p and q determines the trading price. For instance, if the position is initialized with reserves (0, R_2), the position will accept a trade that updates its reserves to R_1 = (q/p)*R_2. In other words, the position is offering up to R_2 of asset 2 at price q/p. More details can be found in the Concentrated Liquidity (opens in a new tab) section of the protocol specification.

The DEX engine indexes positions by pair and by effective price (inclusive of fee tier) and routes trades across the liquidity graph. More details on routing can be found in the On-chain Routing (opens in a new tab) section of the protocol specification.

Position Lifecycle

The lifecycle of a liquidity position is:

Opened

On creation (by a PositionOpen action (opens in a new tab)), the position is in the "opened" state, in which it is actively providing liquidity to the DEX engine. Trade executions against the position update its reserves.

Closed

An opened position can be closed explicitly, by a user-submitted PositionClose action (opens in a new tab), or implicitly by the chain, if the Position (opens in a new tab)'s close_on_fill field is set to true and the position's reserves were completely consumed by a trade.

When a position is closed, it no longer provides liquidity to the DEX and becomes "inert".

Withdrawn

Closing a position does not immediately withdraw funds, because Penumbra transactions (like any ZK transaction model) are early-binding: the prover must know the state transition they prove knowledge of, and they cannot know the final reserves with certainty until after the position has been deactivated.

Instead, reserves are explicitly withdrawn following closure.

Control over liquidity positions is recorded using LPNFTs that track both the position ID and its state. This allows using the transaction's value balance mechanism to track the state machine:

  • the PositionOpen action consumes the initial reserves and produces an "opened-LPNFT";
  • the PositionClose action consumes an opened-LPNFT and produces a "closed-LPNFT";
  • the PositionWithdraw action consumes a closed- or withdrawn-LPNFT and produces a withdrawn-LPNFT with an incremented withdrawal sequence number and any reserves withdrawn from the position.

The protocol supports withdrawing from a position multiple times, though there is currently no reason to do this as the reserves are consumed on the first withdrawal.

This design opens interesting possibilities not possible on a conventional DEX:

  • Block-scoped JIT liquidity: a position can be opened and closed in the same transaction. As the opening is processed in a batch in the first phase of the DEX EndBlock handler and the closure is processed in a batch following all trade executions, this allows a liquidity provider to quote a price and be guaranteed that it will only be valid for a single block's batch executions.

  • Replicating Market Makers: the payoff function of any CFMM can be approximated via constant-sum AMMs, allowing market makers to replicate whatever trading function they want. This functionality is implemented in pcli for two strategies: a linear spread across a range, and an approximation of the payoff of an xy=k (UniV2) curve.

Managing Liquidity Positions

ℹ️

There is currently no GUI support for managing liquidity positions.

Individual limit orders with pcli

pcli can create positions replicating limit order behavior using pcli tx lp order buy and pcli tx lp order sell. For instance:

pcli tx lp order sell 100gm@1gn # sells 100 gm at 1 gn per gm
pcli tx lp order sell 100gm@1gn/50bps # sells 100gm at 1 gn per gm, with 50bps fee

These positions will remain open following execution. For instance, if a user traded 100gn to gm against the first position, its new reserves would be 100gn, which it would offer at 1gm/gn. If this is not desired, the --auto-close parameter can be used:

pcli tx lp order sell 100gm@1gn --auto-close # sells 100 gm at 1 gn per gm, closing on fill

A "buy" order can also be expressed using lp order buy:

pcli tx lp order buy 100gm@2gn --auto-close # sells 200gn at 0.5gm per gn, closing on fill

There is no actual distinction at the protocol level between "buy" and "sell" orders; this command will create a position with initial reserves of 200gn, equivalent to

pcli tx lp order sell 200gn@0.5gm --auto-close

Owned positions can be queried with pcli v balance. Positions can be closed and withdrawn individually or all at once:

pcli tx lp close plpid1tvj0vpdxhuu0tjxsskgf05f6l6dp0xqjqvywptrs0flvvvhvecjqtaxvf7
pcli tx lp withdraw plpid1tvj0vpdxhuu0tjxsskgf05f6l6dp0xqjqvywptrs0flvvvhvecjqtaxvf7
pcli tx lp close-all
pcli tx lp withdraw-all

Range liquidity with pcli

As of v0.80.1, pcli can compute a set of positions that spread liquidity across a price range. To do this, use

pcli tx lp replicate linear --help

This strategy takes a number of parameters (see --help for details), most importantly:

  • the trading pair
  • the lower bound of the price range
  • the upper bound of the price range
  • the fee tier for the positions

For example, the following invocation will create 5 positions spread across the range 1.8 to 2.2 USDC per UM:

$ pcli tx lp replicate linear upenumbra:transfer/channel-2/uusdc 100000000transfer/channel-2/uusdc --lower-price 1.8 --upper-price 2.2 --fee-bps 100 --num-positions 5
#################################################################################
########################### LIQUIDITY SUMMARY ###################################
#################################################################################

You want to provide liquidity on the pair upenumbra:transfer/channel-2/uusdc
You will need:
 -> 50252144upenumbra
 -> 0transfer/channel-2/uusdc
You will create the following positions:
 ID                                                                State      Fee                       Sell Price           Reserves
 plpid1klkeg9ve0mu5hu4v5afrg8qaneh3tmhzjww9c4yd7203v89qv90q0ruva3  opened  100bps  1.800001transfer/channel-2/usdc  11.111111penumbra
 plpid1d5dmxgtaf34k3846kw858wqzyamg40ks20tprzxe0sgdyqcqdfzqj580m7  opened  100bps  1.900001transfer/channel-2/usdc  10.526315penumbra
 plpid1vynfad7qzccl0p3zlqjttfv9smpdlxw86w0c7hxq2fcll9qqlpdqaqhjwv  opened  100bps         2transfer/channel-2/usdc         10penumbra
 plpid1kn5y806w98n0gnr28vq79r0fx7nd0dlclzzsktrla47efhxy87usuzxnae  opened  100bps  2.100001transfer/channel-2/usdc   9.523809penumbra
 plpid1c95eyegaeganw25c79q0etrryneg0ppgy5c9jxv5vh0e0p4yxeqs9a4dtr  opened  100bps  2.200001transfer/channel-2/usdc   9.090909penumbra
Do you want to open those liquidity positions on-chain? [y/n]

The portfolio value function of this strategy (not accounting for fees) can be visualized as follows:

The x-axis shows the price of UM in terms of USDC, and the y-axis shows the value of the position in the presence of traders (i.e., assuming the portfolio always holds the less valuable asset, as a trader will trade away the more valuable asset). The lower part of the chart shows the value of each individual liquidity position. When the market price is less than the position's price, the position holds UM, so the position's value increases with the price of UM up to its sell price, at which point the position's value is constant. The upper part of the chart shows the combined PVF of the portfolio of positions (in blue), as well as the PVF of a UniV3 position in the same range for reference. Notice that even with only 5 positions, the portfolio value fairly closely approximates the UniV3 curve.

This can be understood as a special case of some of the contents of the Replicating Market Makers (opens in a new tab) paper.

⚠️

pcli currently has limited support for external asset metadata, including denom exponents. Double-check proposed positions to be sure they're not mispriced by a factor of 1,000,000.

ℹ️

pcli can load an external registry of asset metadata, placed in $PCLI_HOME/registry.json. The Prax wallet registry can be downloaded via

wget -O PATH/TO/PCLI_HOME/registry.json https://raw.githubusercontent.com/prax-wallet/registry/main/registry/chains/penumbra-1.json
⚠️

Even using an external registry, pcli cannot use it to parse command-line arguments, due to limitations that will be removed in a future release.

Thus it is necessary to specify upenumbra:transfer/channel-2/uusdc (the base denom for both assets, so that the specified prices are 1.8 upenumbra per uusdc, not 1.8 penumbra per uusdc, a very different price) and 100000000transfer/channel-2/uusdc.

This limitation will be removed in a future release.

Programmatic access with pclientd

💡

This section is incomplete. You can help by expanding it!

Programmatic access to a wallet's private state and transaction construction is possible using pclientd. See the pclientd section of the guide for details. This allows client software to work only with a local GRPC endpoint and not have to worry about any Penumbra-specific cryptography or ZK proving.

Managing liquidity with Hummingbot

💡

This section is incomplete. You can help by expanding it!

A work-in-progress Hummingbot connector backed by pclientd exists here (opens in a new tab). Notes on configuration exist here (opens in a new tab).