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
andq
; - the percentage fee applied to trades;
- the position's reserves,
R_1
andR_2
; - a
close_on_fill
boolean, controlling whether the position is closed when a trade completely consumes eitherR_1
orR_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 anxy=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).