From a5aadf13051d3a795135f8de2b2bf91d48d5a53f Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 15:10:05 -0500 Subject: [PATCH 01/10] dex(state_key): add lookup/sorted indices for volume --- crates/core/component/dex/src/state_key.rs | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index 6ce4e5ccb6..b920bea4ed 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -119,6 +119,82 @@ pub fn aggregate_value() -> &'static str { "dex/aggregate_value" } +pub mod lqt { + pub mod v1 { + pub mod lp { + pub mod lookup { + use penumbra_sdk_asset::asset; + + pub(crate) fn _prefix(epoch_index: u64) -> String { + format!("dex/lqt/v1/lp/lookup/{epoch_index:020}/") + } + + // A lookup index used to update cumulative volumes. + /// It maps an trading pair (staking token, asset) to the cumulative volume of outbound liquidity. + /// + /// # Key Encoding + /// The lookup key is encoded as `prefix || asset` + /// # Value Encoding + /// The value is encoded as `BE(Amount)` + pub(crate) fn _volume_by_pair(epoch_index: u64, asset: asset::Id) -> [u8; 74] { + let prefix_bytes = _prefix(epoch_index); + let mut key = [0u8; 74]; + key[0..42].copy_from_slice(prefix_bytes.as_bytes()); + key[42..42 + 32].copy_from_slice(&asset.to_bytes()); + key + } + } + + pub mod by_volume { + use anyhow::{ensure, Result}; + use penumbra_sdk_asset::asset; + use penumbra_sdk_num::Amount; + + pub fn prefix(epoch_index: u64) -> String { + format!("dex/lqt/v1/lp/by_volume/{epoch_index:020}/") + } + + /// Tracks the cumulative volume of outbound liquidity for a given pair. + /// The pair is always connected by the staking token, which is the implicit numeraire. + /// + /// # Encoding + /// The full key is encoded as: `prefix || asset || BE(volume)` + pub(crate) fn _key( + epoch_index: u64, + asset: &asset::Id, + volume: Amount, + ) -> [u8; 93] { + let prefix_bytes = prefix(epoch_index); + let mut key = [0u8; 93]; + key[0..45].copy_from_slice(prefix_bytes.as_bytes()); + key[45..45 + 32].copy_from_slice(&asset.to_bytes()); + key[45 + 32..45 + 32 + 16].copy_from_slice(&(!volume).to_be_bytes()); + key + } + + /// Extract the cumulative amount of liquidity from a fully specified key. + /// + /// # Errors + /// This function will return an error if the key is not 72 bytes. Or, if the + /// key contains an invalid asset identifier. + pub(crate) fn _parse_key(key: &[u8]) -> Result<(asset::Id, Amount)> { + ensure!(key.len() == 93, "key must be 93 bytes"); + + // skip the first 45 bytes of prefix + let raw_asset: [u8; 32] = key[45..45 + 32].try_into()?; + let asset: asset::Id = raw_asset.try_into()?; + + let raw_amount: [u8; 16] = key[45 + 32..45 + 32 + 16].try_into()?; + let amount_complement = Amount::from_be_bytes(raw_amount); + let amount = !amount_complement; + + Ok((asset, amount)) + } + } + } + } +} + pub(crate) mod engine { use super::*; use crate::lp::BareTradingFunction; From 4adca18628cb693fa4806dfd005e636dc24c36b0 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 15:10:39 -0500 Subject: [PATCH 02/10] dex(volume_tracker): stub out cumvol tracking --- .../position_manager/volume_tracker.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 crates/core/component/dex/src/component/position_manager/volume_tracker.rs diff --git a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs new file mode 100644 index 0000000000..96c1779ffd --- /dev/null +++ b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use cnidarium::StateWrite; +use penumbra_sdk_num::Amount; +use position::State::*; +use tracing::instrument; + +use crate::lp::position::{self, Position}; +use crate::state_key::engine; +use crate::DirectedTradingPair; +use async_trait::async_trait; +use penumbra_sdk_proto::{StateReadProto, StateWriteProto}; + +#[async_trait] +pub(crate) trait PositionVolumeTracker: StateWrite { + async fn increase_volume_index( + &mut self, + id: &position::Id, + prev_state: &Option, + new_state: &Position, + ) -> Result<()> { + unimplemented!("increase_volume_index") + } +} + +impl PositionVolumeTracker for T {} + +trait Inner: StateWrite { + #[instrument(skip(self))] + async fn update_volume( + &mut self, + id: &position::Id, + pair: DirectedTradingPair, + old_volume: Amount, + new_volume: Amount, + ) -> Result<()> { + Ok(()) + } +} + +impl Inner for T {} From cb5c7d158718cc57fb8c3b00154045c3f55a0505 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 15:10:59 -0500 Subject: [PATCH 03/10] dex(position_manager): pub crate the volume tracker for now --- crates/core/component/dex/src/component/position_manager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index bb042013d4..daa02bd31b 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -39,6 +39,7 @@ mod base_liquidity_index; pub(crate) mod counter; pub(crate) mod inventory_index; pub(crate) mod price_index; +pub(crate) mod volume_tracker; #[async_trait] pub trait PositionRead: StateRead { From abcc69aaacb8fe380918b25d50d251cfe7c0528d Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 15:15:11 -0500 Subject: [PATCH 04/10] dex: fix comment on state key roughpl --- crates/core/component/dex/src/state_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index b920bea4ed..eac0184806 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -158,7 +158,7 @@ pub mod lqt { /// The pair is always connected by the staking token, which is the implicit numeraire. /// /// # Encoding - /// The full key is encoded as: `prefix || asset || BE(volume)` + /// The full key is encoded as: `prefix || asset || BE(!volume)` pub(crate) fn _key( epoch_index: u64, asset: &asset::Id, From 300bd4aed782f16a2cc7294c88fafa64c91623c1 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 17:21:09 -0500 Subject: [PATCH 05/10] dex: work out the DEX LQT api --- .../core/component/dex/src/component/dex.rs | 2 +- .../core/component/dex/src/component/lqt.rs | 65 ++++++++++++++++ .../core/component/dex/src/component/mod.rs | 2 + crates/core/component/dex/src/state_key.rs | 78 ++++++++++++++----- crates/core/component/dex/src/trading_pair.rs | 4 + 5 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 crates/core/component/dex/src/component/lqt.rs diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index ddb7c86d3e..37db59f7ab 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -14,7 +14,7 @@ use penumbra_sdk_proto::{DomainType as _, StateReadProto, StateWriteProto}; use tendermint::v0_37::abci; use tracing::instrument; -use crate::state_key::block_scoped; +use crate::state_key::{block_scoped,}; use crate::{ component::SwapDataRead, component::SwapDataWrite, event, genesis, state_key, BatchSwapOutputData, DexParameters, DirectedTradingPair, SwapExecution, TradingPair, diff --git a/crates/core/component/dex/src/component/lqt.rs b/crates/core/component/dex/src/component/lqt.rs new file mode 100644 index 0000000000..b2648c1268 --- /dev/null +++ b/crates/core/component/dex/src/component/lqt.rs @@ -0,0 +1,65 @@ +use crate::lp::position; +use crate::state_key::lqt; +use anyhow::Result; +use async_trait::async_trait; +use cnidarium::StateRead; +use futures::StreamExt; +use penumbra_sdk_asset::asset; +use penumbra_sdk_num::Amount; +use penumbra_sdk_proto::StateReadProto; +use penumbra_sdk_sct::component::clock::EpochRead; +use std::pin::Pin; + +/// Provides public read access to LQT data. +#[async_trait] +pub trait LqtRead: StateRead { + /// Returns the cumulative volume of staking token for a trading pair. + /// This is the sum of the outflows of the staking token from all positions in the pair. + /// + /// Default to zero if no volume is found. + async fn get_volume_for_pair(&self, asset: asset::Id) -> Amount { + let epoch = self.get_current_epoch().await.expect("epoch is always set"); + let key = lqt::v1::pair::lookup::volume_by_pair(epoch.index, asset); + let value = self.nonverifiable_get(&key).await.unwrap_or_default(); + value.unwrap_or_default() + } + + /// Returns the cumulative volume of staking token for a given position id. + /// This is the sum of the outflows of the staking token from the position. + /// + /// Default to zero if no volume is found. + async fn get_volume_for_position(&self, position_id: &position::Id) -> Amount { + let epoch = self.get_current_epoch().await.expect("epoch is always set"); + let key = lqt::v1::lp::lookup::volume_by_position(epoch.index, position_id); + let value = self.nonverifiable_get(&key).await.unwrap_or_default(); + value.unwrap_or_default() + } + + /// Returns a stream of position ids sorted by descending volume. + /// The volume is the sum of the outflows of the staking token from the position. + fn positions_by_volume_stream( + &self, + epoch_index: u64, + asset_id: asset::Id, + ) -> Result< + Pin< + Box< + dyn futures::Stream> + + Send + + 'static, + >, + >, + > { + let key = lqt::v1::lp::by_volume::prefix_with_asset(epoch_index, &asset_id); + Ok(self + .nonverifiable_prefix_raw(&key) + .map(|res| { + res.map(|(raw_entry, _)| { + let (asset, volume, position_id) = + lqt::v1::lp::by_volume::parse_key(&raw_entry).expect("TODO"); + (asset, position_id, volume) + }) + }) + .boxed()) + } +} diff --git a/crates/core/component/dex/src/component/mod.rs b/crates/core/component/dex/src/component/mod.rs index 454f1ca53d..7512a8dc23 100644 --- a/crates/core/component/dex/src/component/mod.rs +++ b/crates/core/component/dex/src/component/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod circuit_breaker; mod dex; mod eviction_manager; mod flow; +mod lqt; mod position_manager; mod swap_manager; @@ -20,6 +21,7 @@ pub use dex::{Dex, StateReadExt, StateWriteExt}; pub use position_manager::PositionManager; // Read data from the Dex component; +pub use lqt::LqtRead; pub use position_manager::PositionRead; pub use swap_manager::SwapDataRead; diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index eac0184806..76dc4399d6 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -121,26 +121,52 @@ pub fn aggregate_value() -> &'static str { pub mod lqt { pub mod v1 { - pub mod lp { + pub mod pair { pub mod lookup { use penumbra_sdk_asset::asset; - pub(crate) fn _prefix(epoch_index: u64) -> String { - format!("dex/lqt/v1/lp/lookup/{epoch_index:020}/") + pub(crate) fn prefix(epoch_index: u64) -> String { + format!("dex/lqt/v1/pair/lookup/{epoch_index:020}/") } - // A lookup index used to update cumulative volumes. - /// It maps an trading pair (staking token, asset) to the cumulative volume of outbound liquidity. + // A lookup index used to inspect aggregate outflows for a given pair. + /// It maps a trading pair (staking token, asset) to the cumulative volume of outbound liquidity. /// /// # Key Encoding /// The lookup key is encoded as `prefix || asset` /// # Value Encoding /// The value is encoded as `BE(Amount)` - pub(crate) fn _volume_by_pair(epoch_index: u64, asset: asset::Id) -> [u8; 74] { - let prefix_bytes = _prefix(epoch_index); + pub(crate) fn volume_by_pair(epoch_index: u64, asset: asset::Id) -> [u8; 76] { + let prefix_bytes = prefix(epoch_index); + let mut key = [0u8; 76]; + key[0..44].copy_from_slice(prefix_bytes.as_bytes()); + key[44..44 + 32].copy_from_slice(&asset.to_bytes()); + key + } + } + } + + pub mod lp { + pub mod lookup { + pub(crate) fn prefix(epoch_index: u64) -> String { + format!("dex/lqt/v1/lp/lookup/{epoch_index:020}/") + } + + /// A lookup index used to update the `by_volume` index. + /// It maps a position id to the latest tally of outbound cumulative volume. + /// + /// # Key Encoding + /// The lookup key is encoded as `prefix || position_id` + /// # Value Encoding + /// The value is encoded as `BE(Amount)` + pub(crate) fn volume_by_position( + epoch_index: u64, + position_id: &crate::lp::position::Id, + ) -> [u8; 74] { + let prefix_bytes = prefix(epoch_index); let mut key = [0u8; 74]; key[0..42].copy_from_slice(prefix_bytes.as_bytes()); - key[42..42 + 32].copy_from_slice(&asset.to_bytes()); + key[42..42 + 32].copy_from_slice(&position_id.0); key } } @@ -150,37 +176,49 @@ pub mod lqt { use penumbra_sdk_asset::asset; use penumbra_sdk_num::Amount; + use crate::lp::position; + pub fn prefix(epoch_index: u64) -> String { format!("dex/lqt/v1/lp/by_volume/{epoch_index:020}/") } + pub fn prefix_with_asset(epoch_index: u64, asset: &asset::Id) -> [u8; 74] { + let prefix = prefix(epoch_index); + let mut key = [0u8; 74]; + key[0..42].copy_from_slice(prefix.as_bytes()); + key[42..42 + 32].copy_from_slice(&asset.to_bytes()); + key + } + /// Tracks the cumulative volume of outbound liquidity for a given pair. /// The pair is always connected by the staking token, which is the implicit numeraire. /// /// # Encoding - /// The full key is encoded as: `prefix || asset || BE(!volume)` + /// The full key is encoded as: `prefix || asset || BE(!volume) || position` pub(crate) fn _key( epoch_index: u64, asset: &asset::Id, volume: Amount, - ) -> [u8; 93] { + position: &position::Id, + ) -> [u8; 125] { let prefix_bytes = prefix(epoch_index); - let mut key = [0u8; 93]; + let mut key = [0u8; 125]; key[0..45].copy_from_slice(prefix_bytes.as_bytes()); key[45..45 + 32].copy_from_slice(&asset.to_bytes()); key[45 + 32..45 + 32 + 16].copy_from_slice(&(!volume).to_be_bytes()); + key[45 + 32 + 16..45 + 32 + 16 + 32].copy_from_slice(&position.0); key } - /// Extract the cumulative amount of liquidity from a fully specified key. + /// Parse a raw key into its constituent parts. /// /// # Errors - /// This function will return an error if the key is not 72 bytes. Or, if the - /// key contains an invalid asset identifier. - pub(crate) fn _parse_key(key: &[u8]) -> Result<(asset::Id, Amount)> { - ensure!(key.len() == 93, "key must be 93 bytes"); + /// This function will return an error if the key is not 125 bytes. Or, if the + /// key contains an invalid asset or position identifier. + pub(crate) fn parse_key(key: &[u8]) -> Result<(asset::Id, Amount, position::Id)> { + ensure!(key.len() == 125, "key must be 125 bytes"); - // skip the first 45 bytes of prefix + // Skip the first 45 bytes, which is the prefix. let raw_asset: [u8; 32] = key[45..45 + 32].try_into()?; let asset: asset::Id = raw_asset.try_into()?; @@ -188,7 +226,11 @@ pub mod lqt { let amount_complement = Amount::from_be_bytes(raw_amount); let amount = !amount_complement; - Ok((asset, amount)) + let raw_position_id: [u8; 32] = + key[45 + 32 + 16..45 + 32 + 16 + 32].try_into()?; + let position_id = position::Id(raw_position_id); + + Ok((asset, amount, position_id)) } } } diff --git a/crates/core/component/dex/src/trading_pair.rs b/crates/core/component/dex/src/trading_pair.rs index 936c3ec675..82ee615917 100644 --- a/crates/core/component/dex/src/trading_pair.rs +++ b/crates/core/component/dex/src/trading_pair.rs @@ -102,6 +102,10 @@ impl TradingPair { self.asset_2 } + pub fn contains(&self, asset_id: asset::Id) -> bool { + self.asset_1 == asset_id || self.asset_2 == asset_id + } + /// Convert the trading pair to bytes. pub(crate) fn to_bytes(self) -> [u8; 64] { let mut result: [u8; 64] = [0; 64]; From 3b92361059d854af7f5e2b20e67b53453ae9dc3e Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 17:50:44 -0500 Subject: [PATCH 06/10] dex(volume_tracker): implement inner index logic --- .../core/component/dex/src/component/dex.rs | 2 +- .../core/component/dex/src/component/lqt.rs | 2 + .../position_manager/volume_tracker.rs | 113 ++++++++++++++++-- crates/core/component/dex/src/state_key.rs | 4 +- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index 37db59f7ab..ddb7c86d3e 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -14,7 +14,7 @@ use penumbra_sdk_proto::{DomainType as _, StateReadProto, StateWriteProto}; use tendermint::v0_37::abci; use tracing::instrument; -use crate::state_key::{block_scoped,}; +use crate::state_key::block_scoped; use crate::{ component::SwapDataRead, component::SwapDataWrite, event, genesis, state_key, BatchSwapOutputData, DexParameters, DirectedTradingPair, SwapExecution, TradingPair, diff --git a/crates/core/component/dex/src/component/lqt.rs b/crates/core/component/dex/src/component/lqt.rs index b2648c1268..7f921a067f 100644 --- a/crates/core/component/dex/src/component/lqt.rs +++ b/crates/core/component/dex/src/component/lqt.rs @@ -63,3 +63,5 @@ pub trait LqtRead: StateRead { .boxed()) } } + +impl LqtRead for T {} diff --git a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs index 96c1779ffd..4c3d8efc27 100644 --- a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs +++ b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs @@ -1,24 +1,103 @@ +#![allow(unused_imports, unused_variables, dead_code)] use anyhow::Result; use cnidarium::StateWrite; +use penumbra_sdk_asset::{asset, STAKING_TOKEN_ASSET_ID}; use penumbra_sdk_num::Amount; use position::State::*; use tracing::instrument; +use crate::component::lqt::LqtRead; use crate::lp::position::{self, Position}; -use crate::state_key::engine; -use crate::DirectedTradingPair; +use crate::state_key::{engine, lqt}; +use crate::{trading_pair, DirectedTradingPair, TradingPair}; use async_trait::async_trait; use penumbra_sdk_proto::{StateReadProto, StateWriteProto}; +use penumbra_sdk_sct::component::clock::EpochRead; #[async_trait] pub(crate) trait PositionVolumeTracker: StateWrite { async fn increase_volume_index( &mut self, - id: &position::Id, + position_id: &position::Id, prev_state: &Option, new_state: &Position, - ) -> Result<()> { - unimplemented!("increase_volume_index") + ) { + // We only index the volume for staking token pairs. + if !new_state.phi.pair.contains(*STAKING_TOKEN_ASSET_ID) { + return; + } + + // Or if the position has existed before. + if prev_state.is_none() { + tracing::debug!(?position_id, "newly opened position, skipping volume index"); + return; + } + + // Short-circuit if the position is transitioning to a non-open state. + // This might miss some volume updates, but is more conservative on state-flow. + if !matches!(new_state.state, position::State::Opened) { + tracing::debug!( + ?position_id, + "new state is not `Opened`, skipping volume index" + ); + return; + } + + let trading_pair = new_state.phi.pair.clone(); + + // We want to track the **outflow** of staking tokens from the position. + // This means that we track the amount of staking tokens that have left the position. + // We do this by comparing the previous and new reserves of the staking token. + // We **DO NOT** want to track the volume of the other asset denominated in staking tokens. + let prev_r1 = prev_state + .as_ref() + .map_or(Amount::zero(), |prev| new_state.reserves_1().amount); + + let prev_r2 = prev_state + .as_ref() + .map_or(Amount::zero(), |prev| new_state.reserves_2().amount); + + let new_r1 = new_state.reserves_1().amount; + let new_r2 = new_state.reserves_2().amount; + + // We track the *outflow* of the staking token. + // "How much inventory has left the position?" + let outflow_1 = prev_r1.saturating_sub(&new_r1); + let outflow_2 = prev_r2.saturating_sub(&new_r2); + + // We select the correct outflow based on the staking token asset id. + // This is the amount of volume we aggregate in the volume index. + let staking_token_outflow = if *STAKING_TOKEN_ASSET_ID == trading_pair.asset_1() { + outflow_1 + } else { + outflow_2 + }; + + // We lookup the previous volume index entry. + let old_volume = self.get_volume_for_position(position_id).await; + let new_volume = old_volume.saturating_add(&staking_token_outflow); + + // Grab the ambient epoch index. + let epoch_index = self + .get_current_epoch() + .await + .expect("epoch is always set") + .index; + + // Find the trading pair asset that is not the staking token. + let other_asset = if trading_pair.asset_1() == *STAKING_TOKEN_ASSET_ID { + trading_pair.asset_2() + } else { + trading_pair.asset_1() + }; + + self.update_volume( + epoch_index, + &other_asset, + position_id, + old_volume, + new_volume, + ) } } @@ -26,14 +105,28 @@ impl PositionVolumeTracker for T {} trait Inner: StateWrite { #[instrument(skip(self))] - async fn update_volume( + fn update_volume( &mut self, - id: &position::Id, - pair: DirectedTradingPair, + epoch_index: u64, + asset_id: &asset::Id, + position_id: &position::Id, old_volume: Amount, new_volume: Amount, - ) -> Result<()> { - Ok(()) + ) { + // First, update the lookup index with the new volume. + let lookup_key = lqt::v1::lp::lookup::volume_by_position(epoch_index, position_id); + use penumbra_sdk_proto::StateWriteProto; + self.nonverifiable_put(lookup_key.to_vec(), new_volume); + + // Then, update the sorted index: + let old_index_key = + lqt::v1::lp::by_volume::key(epoch_index, asset_id, position_id, old_volume); + // Delete the old key: + self.nonverifiable_delete(old_index_key.to_vec()); + // Store the new one: + let new_index_key = + lqt::v1::lp::by_volume::key(epoch_index, asset_id, position_id, new_volume); + self.nonverifiable_put(new_index_key.to_vec(), new_volume); } } diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index 76dc4399d6..321830a1ce 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -195,11 +195,11 @@ pub mod lqt { /// /// # Encoding /// The full key is encoded as: `prefix || asset || BE(!volume) || position` - pub(crate) fn _key( + pub(crate) fn key( epoch_index: u64, asset: &asset::Id, - volume: Amount, position: &position::Id, + volume: Amount, ) -> [u8; 125] { let prefix_bytes = prefix(epoch_index); let mut key = [0u8; 125]; From 13dfa4587f4febfd97820464cb856364bf311d19 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 18:15:55 -0500 Subject: [PATCH 07/10] dex(state_key): fix prefix length in `prefix_with_asset` --- crates/core/component/dex/src/state_key.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index 321830a1ce..3eae3ef44d 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -182,11 +182,11 @@ pub mod lqt { format!("dex/lqt/v1/lp/by_volume/{epoch_index:020}/") } - pub fn prefix_with_asset(epoch_index: u64, asset: &asset::Id) -> [u8; 74] { + pub fn prefix_with_asset(epoch_index: u64, asset: &asset::Id) -> [u8; 77] { let prefix = prefix(epoch_index); - let mut key = [0u8; 74]; - key[0..42].copy_from_slice(prefix.as_bytes()); - key[42..42 + 32].copy_from_slice(&asset.to_bytes()); + let mut key = [0u8; 77]; + key[0..45].copy_from_slice(prefix.as_bytes()); + key[45..45 + 32].copy_from_slice(&asset.to_bytes()); key } From 1eef06409ec4efd99b1c69035c71fac37af9bcac Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Tue, 28 Jan 2025 18:19:08 -0500 Subject: [PATCH 08/10] dex: fix mistake when collecting prev reserves --- .../dex/src/component/position_manager/volume_tracker.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs index 4c3d8efc27..fa784479a2 100644 --- a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs +++ b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs @@ -51,11 +51,11 @@ pub(crate) trait PositionVolumeTracker: StateWrite { // We **DO NOT** want to track the volume of the other asset denominated in staking tokens. let prev_r1 = prev_state .as_ref() - .map_or(Amount::zero(), |prev| new_state.reserves_1().amount); + .map_or(Amount::zero(), |prev| prev.reserves_1().amount); let prev_r2 = prev_state .as_ref() - .map_or(Amount::zero(), |prev| new_state.reserves_2().amount); + .map_or(Amount::zero(), |prev| prev.reserves_2().amount); let new_r1 = new_state.reserves_1().amount; let new_r2 = new_state.reserves_2().amount; From 756a6bf57936637dddb4e7501a484b5ad0341e0e Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 29 Jan 2025 13:42:47 -0500 Subject: [PATCH 09/10] lqt(dex): more explicit invariant message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lúcás Meier Signed-off-by: Erwan Or --- crates/core/component/dex/src/component/lqt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/component/dex/src/component/lqt.rs b/crates/core/component/dex/src/component/lqt.rs index 7f921a067f..1b26ef93d2 100644 --- a/crates/core/component/dex/src/component/lqt.rs +++ b/crates/core/component/dex/src/component/lqt.rs @@ -56,7 +56,7 @@ pub trait LqtRead: StateRead { .map(|res| { res.map(|(raw_entry, _)| { let (asset, volume, position_id) = - lqt::v1::lp::by_volume::parse_key(&raw_entry).expect("TODO"); + lqt::v1::lp::by_volume::parse_key(&raw_entry).expect("internal invariant failed: failed to parse state key for lqt::v1::lp::by_volume"); (asset, position_id, volume) }) }) From 4acb6f6e0e0ceff2b02319cb2f1a2aceae3db811 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Wed, 29 Jan 2025 20:57:12 -0500 Subject: [PATCH 10/10] dex: use trading function helpers --- .../position_manager/volume_tracker.rs | 29 ++++++------------- crates/core/component/dex/src/trading_pair.rs | 4 --- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs index fa784479a2..2675a4e2d4 100644 --- a/crates/core/component/dex/src/component/position_manager/volume_tracker.rs +++ b/crates/core/component/dex/src/component/position_manager/volume_tracker.rs @@ -23,7 +23,7 @@ pub(crate) trait PositionVolumeTracker: StateWrite { new_state: &Position, ) { // We only index the volume for staking token pairs. - if !new_state.phi.pair.contains(*STAKING_TOKEN_ASSET_ID) { + if !new_state.phi.matches_input(*STAKING_TOKEN_ASSET_ID) { return; } @@ -49,29 +49,18 @@ pub(crate) trait PositionVolumeTracker: StateWrite { // This means that we track the amount of staking tokens that have left the position. // We do this by comparing the previous and new reserves of the staking token. // We **DO NOT** want to track the volume of the other asset denominated in staking tokens. - let prev_r1 = prev_state - .as_ref() - .map_or(Amount::zero(), |prev| prev.reserves_1().amount); + let prev_state = prev_state.as_ref().expect("the previous state exists"); + let prev_balance = prev_state + .reserves_for(*STAKING_TOKEN_ASSET_ID) + .expect("the staking token is in the pair"); - let prev_r2 = prev_state - .as_ref() - .map_or(Amount::zero(), |prev| prev.reserves_2().amount); - - let new_r1 = new_state.reserves_1().amount; - let new_r2 = new_state.reserves_2().amount; + let new_balance = new_state + .reserves_for(*STAKING_TOKEN_ASSET_ID) + .expect("the staking token is in the pair"); // We track the *outflow* of the staking token. // "How much inventory has left the position?" - let outflow_1 = prev_r1.saturating_sub(&new_r1); - let outflow_2 = prev_r2.saturating_sub(&new_r2); - - // We select the correct outflow based on the staking token asset id. - // This is the amount of volume we aggregate in the volume index. - let staking_token_outflow = if *STAKING_TOKEN_ASSET_ID == trading_pair.asset_1() { - outflow_1 - } else { - outflow_2 - }; + let staking_token_outflow = prev_balance.saturating_sub(&new_balance); // We lookup the previous volume index entry. let old_volume = self.get_volume_for_position(position_id).await; diff --git a/crates/core/component/dex/src/trading_pair.rs b/crates/core/component/dex/src/trading_pair.rs index 82ee615917..936c3ec675 100644 --- a/crates/core/component/dex/src/trading_pair.rs +++ b/crates/core/component/dex/src/trading_pair.rs @@ -102,10 +102,6 @@ impl TradingPair { self.asset_2 } - pub fn contains(&self, asset_id: asset::Id) -> bool { - self.asset_1 == asset_id || self.asset_2 == asset_id - } - /// Convert the trading pair to bytes. pub(crate) fn to_bytes(self) -> [u8; 64] { let mut result: [u8; 64] = [0; 64];