Concentrated Liquidity

Penumbra uses a hybrid, order-book-like AMM with automatic routing. Liquidity on Penumbra is recorded as many individual concentrated liquidity positions, akin to an order book. Each liquidity position is its own AMM, with its own fee tier, and that AMM has the simplest possible form, a constant-sum (fixed-price) market maker. These component AMMs are synthesized into a global AMM by the DEX engine, which optimally routes trades across the entire liquidity graph. Because each component AMM is of the simplest possible form, this optimization problem is easy to solve: it’s a graph traversal.

Liquidity positions

At a high-level, a liquidity position on Penumbra sets aside reserves for one or both assets in a trading pair and specifies a fixed exchange rate between them. The reserves are denoted by and , and the valuations of the assets are denoted by and , respectively. A constant-price pool has a trading function where is a constant.

In practice, a liquidity position consists of:

  • A trading pair recording the asset IDs of the assets in the pair. The asset IDs are elements, and the pair is made order-independent by requiring that .
  • A trading function , specified by .
  • A random, globally-unique 32-byte nonce .

This data is hashed to form the position ID, which uniquely identifies the position. The position nonce ensures that it is not possible to create two positions with colliding position IDs.

The reserves are pointed to by the position ID and recorded separately, as they change over time as trades are executed against the position. One way to think of this is to think of the position ID as an ephemeral account content-addressed by the trading function whose assets are the reserves and which is controlled by bearer NFTs recorded in the shielded pool.

Positions have four position states, and can only progress through them in sequence:

  • an opened position has reserves and can be traded against;
  • a closed position has been deactivated and cannot be traded against, but still has reserves;
  • a withdrawn position has had reserves withdrawn;
  • a claimed position has had any applicable liquidity incentives claimed.

Control over a position is tracked by a liquidity position NFT (LPNFT) that records both the position ID and the position state. Having the LPNFT record both the position state and ID means that the transaction value balance mechanism can be used to enforce state transitions:

  • the PositionOpen action debits the initial reserves and credits an opened position NFT;
  • the PositionClose action debits an opened position NFT and credits a closed position NFT;
  • the PositionWithdraw action debits a closed position NFT and credits a withdrawn position NFT and the final reserves;
  • the PositionRewardClaim action debits a withdrawn position NFT and credits a claimed position NFT and any liquidity incentives.

Separating closed and withdrawn states is necessary because phased execution means that the exact state of the final reserves may not be known until the closure is processed position is removed from the active set.

However, having to wait for the next block to withdraw funds does not necessarily cause a gap in available capital: a marketmaker wishing to update prices block-by-block can stack the PositionWithdraw for the last block’s position with a PositionOpen for their new prices and a PositionClose that expires the new position at the end of the next block.

Separating withdrawn and claimed states allows retroactive liquidity incentives (e.g., rewards over some time window, allocated pro rata to liquidity provided, etc). As yet there are no concrete plans for liquidity incentives, but it seems desirable to build a hook for them, and to allow them to be funded permissionlessly (e.g., so some entity can decide to subsidize liquidity on X pair of their own accord).

The set of all liquidity positions between two assets forms a market, which indicates the availability of inventory at different price levels, just like an order book.

┌────────────────────┐ │ │ │ Asset B │ │ │ └─────┬─┬─┬──┬───────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────┐ │ │ │ │ │ │sell 50A@100, buy 0B@40 │ │ │ │ phi(50,100) - 50*100 + 0*40 = 5000 │ ├────────────────────────┘ │ │ │ │ Asset A │sell 0A@101, buy 25B@43 │ │ │ psi(0,25) = 0*101 + 25*43 = 1075 │ ├──────────────────────────┘ │ │ │ │sell 99A@103, buy 46@38 │ │ chi(99,46) = 99*103 + 46*38 = 11945 │ ├────────────────────────────┘ │ │ │sell 50A@105, buy 1@36 │ om(50,1) = 50*105 + 1*36 = 5286 │ ├───────────────────────────────┘ └───────────┘
┌────────────────────────────────┐ │ x │ │ xx │ │ xxx x │ │ xxx xxx │ │ xxxxx xxxx │ sell 50A@100, buy 0B@40 │ xxxxxx xxxxx │ ──────────────────────── │ xxxxxxx xxxxxx │ sell 0A@101, buy 25B@43 │ xxxxxxxx xxxxxxx │ ──────────────────────── │ xxxxxxxx xxxxxxxx │ sell 99A@103, buy 46@38 │ xxxxxxxxx xxxxxxxxx │ ──────────────────────── │ xxxxxxxxxxx xxxxxxxxxx │ sell 50A@105, buy 1@36 │ xxxxxxxxxxx xxxxxxxxxx │ ──────────────────────── │ xxxxxxxxxxx xxxxxxxxxx │ │ xxxxxxxxxxx xxxxxxxxxxx │ │ xxxxxxxxxxx xxxxxxxxxxxxx │ └────────────────────────────────┘

Liquidity composition

During execution, assets and liquidity positions create a graph that can be traversed. To create and execute routes, it is helpful to understand the available liquidity between two assets, even in the absence of direct liquidity positions between them.

As a result, it is desirable to develop a method for combining two liquidity positions that cover separate but intersecting assets into a unified synthetic position that represents a section of the overall liquidity route. For example, combining positions for the pair A <> B and B <> C into a synthetic position for A <> C.

Given two AMMs, with fee trading between assets and and with fee trading between assets and , we can compose and to obtain a synthetic position trading between assets and that first trades along and then (or along and then ).

We want to write the trading function of this AMM as with fee , prices , and reserves .

First, write the trade inputs and outputs for each AMM as , , , , , , where the subscripts index the asset type and the superscripts index the AMM. We want and , meaning that:

Visually this gives (using as a placeholder for ):

┌─────┬───────────────────────────────────────────────────────────┬─────┐ │ │ ┌───────────────────────────┐ │ │ │ │ │ │ │ │ │ ├──────┐ ┌────┬───┬────┐ │ ┌────┬───┬────┐ │ │ │ │ Δᵡ₁ │ │ │ │ │ │ │ │ │ │ │ │ │ Λᵡ₁ │ │ │ └───►│ Δᵠ₁│ │ Λᵠ₁├──┘ ┌►│ Δᶿ₂│ │ Λᶿ₂├───► 0 └─►│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├─────┤ ├────┤ Φ ├────┤ │ ├────┤ Θ ├────┤ ├─────┤ │ │ │ │ │ ├────┘ │ │ │ │ │ │ │ │ 0 ──►│ Δᵠ₂│ │ Λᵠ₂│ ┌─►│ Δᶿ₃│ │ Λᶿ₃├───────────►│ Λᵡ₃ │ │ Δᵡ₃ │───┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └────┴───┴────┘ │ └────┴───┴────┘ │ │ │ │ └─────────────────────────┘ │ │ └─────┴───────────────────────────────────────────────────────────┴─────┘

The reserves are precisely the maximum possible output . On the one hand, we have , since we cannot obtain more output from than its available reserves. On the other hand, we also have

since we cannot input more into than we can obtain as output from . This means we have

using similar reasoning for as for .

On input , the output is

and similarly on input , the output is

so we can write the trading function of the composition as

with , , fee , and reserves , .