penumbra_sdk_dex/component/eviction_manager.rs
1use crate::component::StateReadExt;
2use crate::{component::position_manager::counter::PositionCounterRead, lp::position};
3use futures::{StreamExt as _, TryStreamExt};
4use std::collections::BTreeSet;
5
6use crate::state_key::eviction_queue;
7use anyhow::Result;
8use cnidarium::StateWrite;
9use tracing::instrument;
10
11use crate::{component::PositionManager, DirectedTradingPair, TradingPair};
12
13pub(crate) trait EvictionManager: StateWrite {
14 /// Evict liquidity positions that are in excess of the trading pair limit.
15 ///
16 /// # Overview
17 /// This method enforce the approximate limit on the number of
18 /// positions that can be active for a given trading pair, as defined
19 /// by [`max_positions_per_pair`](DexParameters#max_positions_per_pair).
20 ///
21 /// # Mechanism
22 ///
23 /// The eviction mechanism functions by inspecting every trading pair which
24 /// had LP opened during the block. For each of them, it computes the "excess"
25 /// amount of positions `M`, defined as follow:
26 /// `M = N - N_max` where N is the number of positions,
27 /// and N_max is a chain parameter.
28 ///
29 /// Since a [`TradingPair`] defines two possible directed pairs, we need
30 /// to ensure that we don't evict LPs if they provide important liquidity
31 /// to at least one direction of the pair.
32 ///
33 /// To do this effectively, we maintain a liquidity index which orders LPs
34 /// by ascending liquidity for each direction of a trading pair. This allow
35 /// us to easily fetch the worst M positions for each index, and only evict
36 /// overlapping LPs.
37 ///
38 /// This approach sidestep the problem of adjudicating which position deserve
39 /// to be preserved depending on the trading direction, and limit the number
40 /// of reads to 2*M. On the other hand, it means that the actual maximum number
41 /// of positions per pair is 2*N_max.
42 ///
43 /// ## Diagram
44 ///
45 /// Q_1: A -> B
46 /// ╔════════════╦━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
47 /// ║ Bottom M ┃ M+1 │ M+2 │ . . . │ N-1 │ N ┃
48 /// ╚════════════╩━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
49 /// ▽▼▼▼▼▼▼▼▽▽▼▽
50 /// ┌─▶ find overlap
51 /// │ ▲▲▲▲△△△▲▲▲▲▲ Q_2: B -> A
52 /// │ ╔════════════╦━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
53 /// │ ║ Bottom M ┃ M+1 │ M+2 │ . . . │ N-1 │ N ┃
54 /// │ ╚════════════╩━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
55 /// │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▶
56 /// │ Ordered by inventory
57 /// │
58 /// │ ▽▼▼▼▼▼▼▼▽▽▼▽
59 /// └───────▶ ∩ Across both indices, the
60 /// ▲▲▲▲△△△▲▲▲▲▲ bottom M positions overlap
61 /// k times where 0 <= k <= M
62 /// for N < 2*N_max
63 ///
64 #[instrument(skip_all, err, level = "trace")]
65 async fn evict_positions(&mut self) -> Result<()> {
66 let hot_pairs: BTreeSet<TradingPair> = self.get_active_trading_pairs_in_block();
67 let max_positions_per_pair = self.get_dex_params().await?.max_positions_per_pair;
68
69 for pair in hot_pairs.iter() {
70 let total_positions = self.get_position_count(pair).await;
71 let overhead_size = total_positions.saturating_sub(max_positions_per_pair);
72 if overhead_size == 0 {
73 continue;
74 }
75
76 let pair_ab = DirectedTradingPair::new(pair.asset_1(), pair.asset_2());
77 let pair_ba = pair_ab.flip();
78 let key_ab = eviction_queue::inventory_index::by_trading_pair(&pair_ab);
79 let key_ba = eviction_queue::inventory_index::by_trading_pair(&pair_ba);
80
81 let stream_ab = self.nonverifiable_prefix_raw(&key_ab).boxed();
82 let stream_ba = self.nonverifiable_prefix_raw(&key_ba).boxed();
83
84 let overhead_ab = stream_ab
85 .take(overhead_size as usize)
86 .and_then(|(k, _)| async move {
87 let raw_id = eviction_queue::inventory_index::parse_id_from_key(k)?;
88 Ok(position::Id(raw_id))
89 })
90 .try_collect::<BTreeSet<position::Id>>()
91 .await?;
92
93 let overhead_ba = stream_ba
94 .take(overhead_size as usize)
95 .and_then(|(k, _)| async move {
96 let raw_id = eviction_queue::inventory_index::parse_id_from_key(k)?;
97 Ok(position::Id(raw_id))
98 })
99 .try_collect::<BTreeSet<position::Id>>()
100 .await?;
101
102 let overlap = overhead_ab.intersection(&overhead_ba);
103
104 for id in overlap {
105 self.close_position_by_id(id).await?;
106 }
107 }
108 Ok(())
109 }
110}
111
112impl<T: StateWrite + ?Sized> EvictionManager for T {}