-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lqt(dex): setup volume trackers (#5016)
## Describe your changes This PR: - expose a component level api `LqtRead` - define two new DEX state key modules: `lqt::v1::lp` and `lqt::v1::pair` - implements a `position_manager::volume_tracker` - stubs out the inner position manager entrypoint, deferring implementation to later ## Volume definition We track the **outflow** of staking tokens from the position. This means that an attacker controlled asset must commit to a staking token inventory for at least a full block execution. ## State key modeling The lookup index maps an epoch index and a position id to a cumulative volume tally. The full sorted index orders position ids by cumulative volume (keyed to the epoch). ## Issue ticket number and link Part of #5015 ## Checklist before requesting a review - [x] I have added guiding text to explain how a reviewer should test these changes. N/A - [x] If this code contains consensus-breaking changes, I have added the "consensus-breaking" label. Otherwise, I declare my belief that there are not consensus-breaking changes, for the following reason: > LQT branch --------- Signed-off-by: Erwan Or <[email protected]> Co-authored-by: Lúcás Meier <[email protected]>
- Loading branch information
1 parent
3b45dc2
commit 943a9a8
Showing
5 changed files
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
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<Item = Result<(asset::Id, position::Id, Amount)>> | ||
+ 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("internal invariant failed: failed to parse state key for lqt::v1::lp::by_volume"); | ||
(asset, position_id, volume) | ||
}) | ||
}) | ||
.boxed()) | ||
} | ||
} | ||
|
||
impl<T: StateRead + ?Sized> LqtRead for T {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
crates/core/component/dex/src/component/position_manager/volume_tracker.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
#![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, 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, | ||
position_id: &position::Id, | ||
prev_state: &Option<Position>, | ||
new_state: &Position, | ||
) { | ||
// We only index the volume for staking token pairs. | ||
if !new_state.phi.matches_input(*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_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 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 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; | ||
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, | ||
) | ||
} | ||
} | ||
|
||
impl<T: StateWrite + ?Sized> PositionVolumeTracker for T {} | ||
|
||
trait Inner: StateWrite { | ||
#[instrument(skip(self))] | ||
fn update_volume( | ||
&mut self, | ||
epoch_index: u64, | ||
asset_id: &asset::Id, | ||
position_id: &position::Id, | ||
old_volume: Amount, | ||
new_volume: Amount, | ||
) { | ||
// 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); | ||
} | ||
} | ||
|
||
impl<T: StateWrite + ?Sized> Inner for T {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters