diff --git a/crates/core/component/dex/src/circuit_breaker/mod.rs b/crates/core/component/dex/src/circuit_breaker/mod.rs deleted file mode 100644 index 6dcbec4acc..0000000000 --- a/crates/core/component/dex/src/circuit_breaker/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod execution; -mod value; - -pub(crate) use execution::ExecutionCircuitBreaker; -pub(crate) use value::ValueCircuitBreaker; diff --git a/crates/core/component/dex/src/circuit_breaker/value.rs b/crates/core/component/dex/src/circuit_breaker/value.rs deleted file mode 100644 index 34a93ae165..0000000000 --- a/crates/core/component/dex/src/circuit_breaker/value.rs +++ /dev/null @@ -1,250 +0,0 @@ -use penumbra_asset::{asset::Id, Balance, Value}; -use penumbra_num::Amount; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ValueCircuitBreaker { - balance: Balance, -} - -impl ValueCircuitBreaker { - pub fn tally(&mut self, balance: Balance) { - self.balance += balance; - } - - pub fn check(&self) -> anyhow::Result<()> { - // No assets should ever be "required" by the circuit breaker's - // internal balance tracking, only "provided". - if let Some(r) = self.balance.required().next() { - return Err(anyhow::anyhow!( - "balance for asset {} is negative: -{}", - r.asset_id, - r.amount - )); - } - - Ok(()) - } - - pub fn available(&self, asset_id: Id) -> Value { - self.balance - .provided() - .find(|b| b.asset_id == asset_id) - .unwrap_or(Value { - asset_id, - amount: Amount::from(0u64), - }) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use crate::component::position_manager::Inner as _; - use crate::component::router::HandleBatchSwaps as _; - use crate::component::{StateReadExt as _, StateWriteExt as _}; - use crate::{ - component::{router::limit_buy, tests::TempStorageExt, PositionManager as _}, - state_key, DirectedUnitPair, - }; - use cnidarium::{ - ArcStateDeltaExt as _, StateDelta, StateRead as _, StateWrite as _, TempStorage, - }; - use penumbra_asset::{asset, Value}; - use penumbra_num::Amount; - use penumbra_proto::StateWriteProto as _; - use rand_core::OsRng; - - use crate::{ - lp::{position::Position, Reserves}, - DirectedTradingPair, - }; - - use super::*; - - // Ideally the update_position_aggregate_value in the PositionManager would be used - // but this is simpler for a quick unit test. - - #[test] - fn value_circuit_breaker() { - let mut value_circuit_breaker = ValueCircuitBreaker::default(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair = DirectedTradingPair::new(gm.id(), gn.id()); - let reserves_1 = Reserves { - r1: 0u64.into(), - r2: 120_000u64.into(), - }; - - // A position with 120_000 gn and 0 gm. - let position_1 = Position::new( - OsRng, - pair, - 9u32, - 1_200_000u64.into(), - 1_000_000u64.into(), - reserves_1, - ); - - // Track the position in the circuit breaker. - let pair = position_1.phi.pair; - let new_a = position_1 - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let new_b = position_1 - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: new_a, - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: new_b, - }); - value_circuit_breaker.tally(new_a); - value_circuit_breaker.tally(new_b.clone()); - - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 120_000u64.into()); - - // The circuit breaker should not trip. - assert!(value_circuit_breaker.check().is_ok()); - - // If the same amount of gn is taken out of the position, the circuit breaker should not trip. - value_circuit_breaker.tally(-new_b); - assert!(value_circuit_breaker.check().is_ok()); - - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 0u64.into()); - - // But if there's ever a negative amount of gn in the position, the circuit breaker should trip. - let one_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: Amount::from(1u64), - }); - value_circuit_breaker.tally(-one_b); - assert!(value_circuit_breaker.check().is_err()); - assert!(value_circuit_breaker.available(pair.asset_1).amount == 0u64.into()); - assert!(value_circuit_breaker.available(pair.asset_2).amount == 0u64.into()); - } - - #[tokio::test] - async fn position_value_circuit_breaker() -> anyhow::Result<()> { - let _ = tracing_subscriber::fmt::try_init(); - let storage = TempStorage::new().await?.apply_minimal_genesis().await?; - let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); - let mut state_tx = state.try_begin_transaction().unwrap(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); - - let one = 1u64.into(); - let price1 = one; - // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let mut buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Update the position to buy 1 gm with 2 gn (i.e. reserves will be 2gn). - buy_1.reserves.r2 = 2u64.into(); - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Pretend the position has been filled against and flipped, so there's no - // gn in the position and there is 2 gm. - buy_1.reserves.r1 = 2u64.into(); - buy_1.reserves.r2 = 0u64.into(); - - // This should not error, the circuit breaker should not trip. - state_tx.put_position(buy_1.clone()).await.unwrap(); - - // Pretend the position was overfilled. - let mut value_circuit_breaker: ValueCircuitBreaker = match state_tx - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => panic!("should have a circuit breaker present"), - }; - - // Wipe out the value in the circuit breaker, so that any outflows should trip it. - value_circuit_breaker.balance = Balance::default(); - state_tx.nonverifiable_put_raw( - state_key::aggregate_value().as_bytes().to_vec(), - serde_json::to_vec(&value_circuit_breaker) - .expect("able to serialize value circuit breaker for nonverifiable storage"), - ); - - // This should error, since there is no balance available to close out the position. - buy_1.state = crate::lp::position::State::Closed; - assert!(state_tx.put_position(buy_1).await.is_err()); - - Ok(()) - } - - #[tokio::test] - #[should_panic(expected = "balance for asset")] - async fn batch_swap_circuit_breaker() { - let _ = tracing_subscriber::fmt::try_init(); - let storage = TempStorage::new() - .await - .expect("able to create storage") - .apply_minimal_genesis() - .await - .expect("able to apply genesis"); - let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); - let mut state_tx = state.try_begin_transaction().unwrap(); - - let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); - let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); - - let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); - - // Manually put a position without calling `put_position` so that the - // circuit breaker is not aware of the position's value. Then, handling a batch - // swap that fills against this position should result in an error. - let one = 1u64.into(); - let price1 = one; - // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). - let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); - - let id = buy_1.id(); - - let position = state_tx.handle_limit_order(&None, buy_1); - state_tx.index_position_by_price(&position); - state_tx - .update_available_liquidity(&position, &None) - .await - .expect("able to update liquidity"); - state_tx.put(state_key::position_by_id(&id), position); - - // Now there's a position in the state, but the circuit breaker is not aware of it. - let trading_pair = pair_1.into_directed_trading_pair().into(); - let mut swap_flow = state_tx.swap_flow(&trading_pair); - - assert!(trading_pair.asset_1() == gm.id()); - - // Add the amount of each asset being swapped to the batch swap flow. - swap_flow.0 += gm.value(5u32.into()).amount; - swap_flow.1 += 0u32.into(); - - // Set the batch swap flow for the trading pair. - state_tx.put_swap_flow(&trading_pair, swap_flow.clone()); - state_tx.apply(); - - // This call should panic due to the outflow of gn not being covered by the circuit breaker. - let routing_params = state.routing_params().await.unwrap(); - state - .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) - .await - .expect("unable to process batch swaps"); - } -} diff --git a/crates/core/component/dex/src/component/action_handler/position/open.rs b/crates/core/component/dex/src/component/action_handler/position/open.rs index 2474d50290..588c8972b7 100644 --- a/crates/core/component/dex/src/component/action_handler/position/open.rs +++ b/crates/core/component/dex/src/component/action_handler/position/open.rs @@ -5,7 +5,7 @@ use cnidarium_component::ActionHandler; use penumbra_proto::StateWriteProto as _; use crate::{ - component::{PositionManager, PositionRead}, + component::{PositionManager, PositionRead, ValueCircuitBreaker}, event, lp::{action::PositionOpen, position}, }; @@ -33,6 +33,13 @@ impl ActionHandler for PositionOpen { async fn check_and_execute(&self, mut state: S) -> Result<()> { // Validate that the position ID doesn't collide state.check_position_id_unused(&self.position.id()).await?; + + // Credit the DEX for the inflows from this position. + // TODO: in a future PR, split current PositionManager to PositionManagerInner + // and fold this into a position open method + state.vcb_credit(self.position.reserves_1()).await?; + state.vcb_credit(self.position.reserves_2()).await?; + state.put_position(self.position.clone()).await?; state.record_proto(event::position_open(self)); Ok(()) diff --git a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs index aed55aa0cd..a012ae4c9c 100644 --- a/crates/core/component/dex/src/component/action_handler/position/withdraw.rs +++ b/crates/core/component/dex/src/component/action_handler/position/withdraw.rs @@ -7,7 +7,7 @@ use decaf377::Fr; use penumbra_proto::StateWriteProto; use crate::{ - component::{PositionManager, PositionRead}, + component::{PositionManager, PositionRead, ValueCircuitBreaker}, event, lp::{action::PositionWithdraw, position, Reserves}, }; @@ -90,6 +90,12 @@ impl ActionHandler for PositionWithdraw { // the current reserves. state.record_proto(event::position_withdraw(self, &metadata)); + // Debit the DEX for the outflows from this position. + // TODO: in a future PR, split current PositionManager to PositionManagerInner + // and fold this into a position open method + state.vcb_debit(metadata.reserves_1()).await?; + state.vcb_debit(metadata.reserves_2()).await?; + // Finally, update the position. This has two steps: // - update the state with the correct sequence number; // - zero out the reserves, to prevent double-withdrawals. diff --git a/crates/core/component/dex/src/component/action_handler/swap.rs b/crates/core/component/dex/src/component/action_handler/swap.rs index b1dce652dc..b735a3b295 100644 --- a/crates/core/component/dex/src/component/action_handler/swap.rs +++ b/crates/core/component/dex/src/component/action_handler/swap.rs @@ -47,7 +47,9 @@ impl ActionHandler for Swap { swap_flow.1 += swap.body.delta_2_i; // Set the batch swap flow for the trading pair. - state.put_swap_flow(&swap.body.trading_pair, swap_flow); + state + .put_swap_flow(&swap.body.trading_pair, swap_flow) + .await?; // Record the swap commitment in the state. let source = state.get_current_source().expect("source is set"); diff --git a/crates/core/component/dex/src/component/arb.rs b/crates/core/component/dex/src/component/arb.rs index b1d9009de5..b802ca2279 100644 --- a/crates/core/component/dex/src/component/arb.rs +++ b/crates/core/component/dex/src/component/arb.rs @@ -8,7 +8,7 @@ use penumbra_proto::StateWriteProto as _; use penumbra_sct::component::clock::EpochRead; use tracing::instrument; -use crate::{event, ExecutionCircuitBreaker, SwapExecution}; +use crate::{component::ExecutionCircuitBreaker, event, SwapExecution}; use super::{ router::{RouteAndFill, RoutingParams}, diff --git a/crates/core/component/dex/src/circuit_breaker/execution.rs b/crates/core/component/dex/src/component/circuit_breaker/execution.rs similarity index 100% rename from crates/core/component/dex/src/circuit_breaker/execution.rs rename to crates/core/component/dex/src/component/circuit_breaker/execution.rs diff --git a/crates/core/component/dex/src/component/circuit_breaker/mod.rs b/crates/core/component/dex/src/component/circuit_breaker/mod.rs new file mode 100644 index 0000000000..448223c17c --- /dev/null +++ b/crates/core/component/dex/src/component/circuit_breaker/mod.rs @@ -0,0 +1,5 @@ +mod execution; +mod value; + +pub use execution::ExecutionCircuitBreaker; +pub use value::ValueCircuitBreaker; diff --git a/crates/core/component/dex/src/component/circuit_breaker/value.rs b/crates/core/component/dex/src/component/circuit_breaker/value.rs new file mode 100644 index 0000000000..eb650207a7 --- /dev/null +++ b/crates/core/component/dex/src/component/circuit_breaker/value.rs @@ -0,0 +1,259 @@ +use anyhow::{anyhow, Result}; +use cnidarium::StateWrite; +use penumbra_asset::Value; +use penumbra_num::Amount; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use tonic::async_trait; + +use crate::{event, state_key}; + +/// Tracks the aggregate value of deposits in the DEX. +#[async_trait] +pub trait ValueCircuitBreaker: StateWrite { + /// Credits a deposit into the DEX. + async fn vcb_credit(&mut self, value: Value) -> Result<()> { + let balance: Amount = self + .get(&state_key::value_balance(&value.asset_id)) + .await? + .unwrap_or_default(); + let new_balance = balance + .checked_add(&value.amount) + .ok_or_else(|| anyhow!("overflowed balance while crediting value circuit breaker"))?; + self.put(state_key::value_balance(&value.asset_id), new_balance); + + self.record_proto(event::vcb_credit(value.asset_id, balance, new_balance)); + Ok(()) + } + + /// Debits a deposit from the DEX. + async fn vcb_debit(&mut self, value: Value) -> Result<()> { + let balance: Amount = self + .get(&state_key::value_balance(&value.asset_id)) + .await? + .unwrap_or_default(); + let new_balance = balance + .checked_sub(&value.amount) + .ok_or_else(|| anyhow!("underflowed balance while debiting value circuit breaker"))?; + self.put(state_key::value_balance(&value.asset_id), new_balance); + + self.record_proto(event::vcb_debit(value.asset_id, balance, new_balance)); + Ok(()) + } +} + +impl ValueCircuitBreaker for T {} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::component::position_manager::Inner as _; + use crate::component::router::HandleBatchSwaps as _; + use crate::component::{StateReadExt as _, StateWriteExt as _}; + use crate::lp::plan::PositionWithdrawPlan; + use crate::{ + component::{router::limit_buy, tests::TempStorageExt, PositionManager as _}, + state_key, DirectedUnitPair, + }; + use crate::{BatchSwapOutputData, PositionOpen}; + use cnidarium::{ArcStateDeltaExt as _, StateDelta, TempStorage}; + use cnidarium_component::ActionHandler as _; + use penumbra_asset::asset; + use penumbra_num::Amount; + use penumbra_proto::StateWriteProto as _; + use penumbra_sct::component::clock::EpochManager as _; + use penumbra_sct::component::source::SourceContext as _; + use penumbra_sct::epoch::Epoch; + + use super::*; + + #[tokio::test] + async fn value_circuit_breaker() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new().await?.apply_minimal_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + let test_usd = asset::Cache::with_known_assets() + .get_unit("test_usd") + .unwrap(); + + // A credit followed by a debit of the same amount should succeed. + // Credit 100 gm. + state_tx.vcb_credit(gm.value(100u64.into())).await?; + // Credit 100 gn. + state_tx.vcb_credit(gn.value(100u64.into())).await?; + + // Debit 100 gm. + state_tx.vcb_debit(gm.value(100u64.into())).await?; + // Debit 100 gn. + state_tx.vcb_debit(gn.value(100u64.into())).await?; + + // Debiting an additional gm should fail. + assert!(state_tx.vcb_debit(gm.value(1u64.into())).await.is_err()); + + // Debiting an asset that hasn't been credited should also fail. + assert!(state_tx + .vcb_debit(test_usd.value(1u64.into())) + .await + .is_err()); + + Ok(()) + } + + #[tokio::test] + async fn position_value_circuit_breaker() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new().await?.apply_minimal_genesis().await?; + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let height = 1; + + // 1. Simulate BeginBlock + + state_tx.put_epoch_by_height( + height, + Epoch { + index: 0, + start_height: 0, + }, + ); + state_tx.put_block_height(height); + state_tx.apply(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + + let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); + + let one = 1u64.into(); + let price1 = one; + // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). + let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + + // Create the PositionOpen action + let pos_open = PositionOpen { + position: buy_1.clone(), + }; + + // Execute the PositionOpen action. + pos_open.check_stateless(()).await?; + pos_open.check_historical(state.clone()).await?; + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + pos_open.check_and_execute(&mut state_tx).await?; + state_tx.apply(); + + // Set the output data for the block to 1 gn and 0 gm. + // This should not error, the circuit breaker should not trip. + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx + .set_output_data( + BatchSwapOutputData { + delta_1: 0u64.into(), + delta_2: 1u64.into(), + lambda_1: 0u64.into(), + lambda_2: 0u64.into(), + unfilled_1: 0u64.into(), + unfilled_2: 0u64.into(), + height: 1, + trading_pair: pair_1.into_directed_trading_pair().into(), + epoch_starting_height: 0, + }, + None, + None, + ) + .await?; + + // Pretend the position was overfilled. + + // Wipe out the gm value in the circuit breaker, so that any outflows should trip it. + state_tx.put(state_key::value_balance(&gm.id()), Amount::from(0u64)); + + // Create the PositionWithdraw action + let pos_withdraw_plan = PositionWithdrawPlan { + position_id: buy_1.id(), + reserves: buy_1.reserves, + sequence: 1, + pair: pair_1.into_directed_trading_pair().into(), + rewards: vec![], + }; + + let pos_withdraw = pos_withdraw_plan.position_withdraw(); + + // Execute the PositionWithdraw action. + pos_withdraw.check_stateless(()).await?; + pos_withdraw.check_historical(state.clone()).await?; + let mut state_tx = state.try_begin_transaction().unwrap(); + state_tx.put_mock_source(1u8); + // This should error, since there is no balance available to withdraw the position. + assert!(pos_withdraw.check_and_execute(&mut state_tx).await.is_err()); + state_tx.apply(); + + Ok(()) + } + + #[tokio::test] + #[should_panic(expected = "underflowed balance while debiting value circuit breaker")] + async fn batch_swap_circuit_breaker() { + let _ = tracing_subscriber::fmt::try_init(); + let storage = TempStorage::new() + .await + .expect("able to create storage") + .apply_minimal_genesis() + .await + .expect("able to apply genesis"); + let mut state = Arc::new(StateDelta::new(storage.latest_snapshot())); + let mut state_tx = state.try_begin_transaction().unwrap(); + + let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap(); + let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap(); + + let pair_1 = DirectedUnitPair::new(gm.clone(), gn.clone()); + + // Manually put a position without calling `put_position` so that the + // circuit breaker is not aware of the position's value. Then, handling a batch + // swap that fills against this position should result in an error. + let one = 1u64.into(); + let price1 = one; + // Create a position buying 1 gm with 1 gn (i.e. reserves will be 1gn). + let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), price1); + + let id = buy_1.id(); + + let position = state_tx.handle_limit_order(&None, buy_1); + state_tx.index_position_by_price(&position); + state_tx + .update_available_liquidity(&position, &None) + .await + .expect("able to update liquidity"); + state_tx.put(state_key::position_by_id(&id), position); + + // Now there's a position in the state, but the circuit breaker is not aware of it. + let trading_pair = pair_1.into_directed_trading_pair().into(); + let mut swap_flow = state_tx.swap_flow(&trading_pair); + + assert!(trading_pair.asset_1() == gm.id()); + + // Add the amount of each asset being swapped to the batch swap flow. + swap_flow.0 += gm.value(5u32.into()).amount; + swap_flow.1 += 0u32.into(); + + // Set the batch swap flow for the trading pair. + state_tx + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); + state_tx.apply(); + + let routing_params = state.routing_params().await.unwrap(); + // This call should panic due to the outflow of gn not being covered by the circuit breaker. + state + .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) + .await + .expect("unable to process batch swaps"); + } +} diff --git a/crates/core/component/dex/src/component/dex.rs b/crates/core/component/dex/src/component/dex.rs index f070d98bd1..38456735d6 100644 --- a/crates/core/component/dex/src/component/dex.rs +++ b/crates/core/component/dex/src/component/dex.rs @@ -18,7 +18,7 @@ use crate::{ use super::{ router::{HandleBatchSwaps, RoutingParams}, - Arbitrage, PositionManager, + Arbitrage, PositionManager, ValueCircuitBreaker, }; pub struct Dex {} @@ -209,12 +209,29 @@ pub trait StateWriteExt: StateWrite + StateReadExt { self.object_put(state_key::config::dex_params_updated(), ()) } - fn set_output_data( + async fn set_output_data( &mut self, output_data: BatchSwapOutputData, swap_execution_1_for_2: Option, swap_execution_2_for_1: Option, - ) { + ) -> Result<()> { + // Debit the DEX for the swap outflows. + // Note that since we credited the DEX for _all_ inflows, we need to debit the + // unfilled amounts as well as the filled amounts. + // + // In the case of a value inflation bug, the debit call will return an underflow + // error, which will halt the chain. + self.vcb_debit(Value { + amount: output_data.unfilled_1 + output_data.lambda_1, + asset_id: output_data.trading_pair.asset_1, + }) + .await?; + self.vcb_debit(Value { + amount: output_data.unfilled_2 + output_data.lambda_2, + asset_id: output_data.trading_pair.asset_2, + }) + .await?; + // Write the output data to the state under a known key, for querying, ... let height = output_data.height; let trading_pair = output_data.trading_pair; @@ -247,17 +264,40 @@ pub trait StateWriteExt: StateWrite + StateReadExt { swap_execution_1_for_2, swap_execution_2_for_1, )); + + Ok(()) } fn set_arb_execution(&mut self, height: u64, execution: SwapExecution) { self.put(state_key::arb_execution(height), execution); } - fn put_swap_flow(&mut self, trading_pair: &TradingPair, swap_flow: SwapFlow) { + async fn put_swap_flow( + &mut self, + trading_pair: &TradingPair, + swap_flow: SwapFlow, + ) -> Result<()> { + // Credit the DEX for the swap inflows. + // + // Note that we credit the DEX for _all_ inflows, since we don't know + // how much will eventually be filled. + self.vcb_credit(Value { + amount: swap_flow.0, + asset_id: trading_pair.asset_1, + }) + .await?; + self.vcb_credit(Value { + amount: swap_flow.1, + asset_id: trading_pair.asset_2, + }) + .await?; + // TODO: replace with IM struct later let mut swap_flows = self.swap_flows(); swap_flows.insert(*trading_pair, swap_flow); - self.object_put(state_key::swap_flows(), swap_flows) + self.object_put(state_key::swap_flows(), swap_flows); + + Ok(()) } } diff --git a/crates/core/component/dex/src/component/mod.rs b/crates/core/component/dex/src/component/mod.rs index 11b8eb217b..871bd67748 100644 --- a/crates/core/component/dex/src/component/mod.rs +++ b/crates/core/component/dex/src/component/mod.rs @@ -8,6 +8,7 @@ pub mod router; mod action_handler; mod arb; +pub(crate) mod circuit_breaker; mod dex; mod flow; pub(crate) mod position_manager; @@ -15,6 +16,8 @@ mod swap_manager; pub use self::metrics::register_metrics; pub use arb::Arbitrage; +pub use circuit_breaker::ExecutionCircuitBreaker; +pub(crate) use circuit_breaker::ValueCircuitBreaker; pub use dex::{Dex, StateReadExt, StateWriteExt}; pub use position_manager::{PositionManager, PositionRead}; pub use swap_manager::SwapManager; diff --git a/crates/core/component/dex/src/component/position_manager.rs b/crates/core/component/dex/src/component/position_manager.rs index a004547db1..9854418fd6 100644 --- a/crates/core/component/dex/src/component/position_manager.rs +++ b/crates/core/component/dex/src/component/position_manager.rs @@ -7,12 +7,11 @@ use async_trait::async_trait; use cnidarium::{EscapedByteSlice, StateRead, StateWrite}; use futures::Stream; use futures::StreamExt; -use penumbra_asset::{asset, Balance, Value}; +use penumbra_asset::asset; use penumbra_num::Amount; use penumbra_proto::DomainType; use penumbra_proto::{StateReadProto, StateWriteProto}; -use crate::circuit_breaker::ValueCircuitBreaker; use crate::lp::position::State; use crate::{ lp::position::{self, Position}, @@ -158,10 +157,6 @@ pub trait PositionManager: StateWrite + PositionRead { // Update the available liquidity for this position's trading pair. self.update_available_liquidity(&position, &prev).await?; - // Update the value circuit breaker's aggregate account. - self.update_position_aggregate_value(&position, &prev) - .await?; - self.put(state_key::position_by_id(&id), position); Ok(()) } @@ -463,140 +458,5 @@ pub(crate) trait Inner: StateWrite { Ok(()) } - - /// Tracks the total token supply deposited in positions for all assets to ensure - /// asset value conservation (i.e. that more assets can't come out of positions than - /// were deposited). - async fn update_position_aggregate_value( - &mut self, - position: &Position, - prev_position: &Option, - ) -> Result<()> { - tracing::debug!( - ?position, - ?prev_position, - "updating position aggregate value" - ); - - // Find the difference in the amounts of assets A and B, based on the state of the position being stored, - // and the previous state of the position. - let (net_change_for_a, net_change_for_b) = match (position.state, prev_position) { - (State::Opened, None) => { - // The position is newly opened, so the change is the full amount of assets A and B. - - // Use the new reserves to compute `new_position_contribution`, - // the amount of asset A contributed by the position (i.e. the reserves of asset A). - let pair = position.phi.pair; - let new_a = position - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let new_b = position - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: new_a, - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: new_b, - }); - (new_a, new_b) - } - (State::Opened, Some(prev)) => { - // The position is still open however the reserves have changed, so the change is the difference - // between the previous reserves and the new reserves. - let pair = position.phi.pair; - let new_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: position - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"), - }); - let new_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: position - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"), - }); - let old_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: prev - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"), - }); - let old_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: prev - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"), - }); - - (new_a - old_a, new_b - old_b) - } - (State::Closed, Some(prev)) => { - // The previous amount of assets A and B should be subtracted from the aggregate value. - - let pair = position.phi.pair; - let old_a = prev - .reserves_for(pair.asset_1) - .expect("specified position should match provided trading pair"); - let old_b = prev - .reserves_for(pair.asset_2) - .expect("specified position should match provided trading pair"); - - let old_a = Balance::from(Value { - asset_id: pair.asset_1, - amount: old_a, - }); - let old_b = Balance::from(Value { - asset_id: pair.asset_2, - amount: old_b, - }); - // The position is closed, so the change is the negative of the previous reserves. - (-old_a, -old_b) - } - (State::Withdrawn { .. }, _) | (State::Closed, None) => { - // The position already went through the `Closed` state or was opened in the `Closed` state, so its contribution has already been subtracted. - return Ok(()); - } - }; - - tracing::debug!( - ?position, - ?net_change_for_a, - ?net_change_for_b, - "updating position assets' aggregate balances" - ); - - let mut value_circuit_breaker: ValueCircuitBreaker = match self - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => ValueCircuitBreaker::default(), - }; - - // Add the change to the value circuit breaker for assets A and B. - value_circuit_breaker.tally(net_change_for_a); - value_circuit_breaker.tally(net_change_for_b); - - // Confirm that the value circuit breaker is still within the limits. - // This call will panic if the value circuit breaker detects inflation. - value_circuit_breaker.check()?; - - // Store the value circuit breaker back to nonconsensus storage with the updated tallies. - self.nonverifiable_put_raw( - state_key::aggregate_value().as_bytes().to_vec(), - serde_json::to_vec(&value_circuit_breaker) - .expect("able to serialize value circuit breaker for nonverifiable storage"), - ); - - Ok(()) - } } impl Inner for T {} diff --git a/crates/core/component/dex/src/component/router/route_and_fill.rs b/crates/core/component/dex/src/component/router/route_and_fill.rs index 19671b4edd..b18a786200 100644 --- a/crates/core/component/dex/src/component/router/route_and_fill.rs +++ b/crates/core/component/dex/src/component/router/route_and_fill.rs @@ -8,14 +8,13 @@ use penumbra_num::Amount; use tracing::instrument; use crate::{ - circuit_breaker::ValueCircuitBreaker, component::{ flow::SwapFlow, router::{FillRoute, PathSearch, RoutingParams}, - PositionManager, StateWriteExt, + ExecutionCircuitBreaker, PositionManager, StateWriteExt, }, lp::position::MAX_RESERVE_AMOUNT, - state_key, BatchSwapOutputData, ExecutionCircuitBreaker, SwapExecution, TradingPair, + BatchSwapOutputData, SwapExecution, TradingPair, }; use super::fill_route::FillError; @@ -49,19 +48,6 @@ pub trait HandleBatchSwaps: StateWrite + Sized { tracing::debug!(?delta_1, ?delta_2, ?trading_pair, "decrypted batch swaps"); let execution_circuit_breaker = ExecutionCircuitBreaker::default(); - // Fetch the ValueCircuitBreaker prior to calling `route_and_fill`, so - // we know the total aggregate amount of each asset prior to executing and - // can ensure the total outflows don't exceed the total balances. - let value_circuit_breaker: ValueCircuitBreaker = match self - .nonverifiable_get_raw(state_key::aggregate_value().as_bytes()) - .await - .expect("able to retrieve value circuit breaker from nonverifiable storage") - { - Some(bytes) => serde_json::from_slice(&bytes).expect( - "able to deserialize stored value circuit breaker from nonverifiable storage", - ), - None => ValueCircuitBreaker::default(), - }; let swap_execution_1_for_2 = if delta_1.value() > 0 { Some( @@ -121,19 +107,6 @@ pub trait HandleBatchSwaps: StateWrite + Sized { unfilled_2, }; - // Check that the output data doesn't exceed the ValueCircuitBreaker's quantities - // (i.e. we didn't outflow more value than existed within liquidity positions). - let available_asset_1 = value_circuit_breaker.available(trading_pair.asset_1()); - let available_asset_2 = value_circuit_breaker.available(trading_pair.asset_2()); - assert!( - output_data.lambda_1 <= available_asset_1.amount, - "asset 1 outflow exceeds available balance" - ); - assert!( - output_data.lambda_2 <= available_asset_2.amount, - "asset 2 outflow exceeds available balance" - ); - // Fetch the swap execution object that should have been modified during the routing and filling. tracing::debug!( ?output_data, @@ -142,7 +115,8 @@ pub trait HandleBatchSwaps: StateWrite + Sized { ); Arc::get_mut(self) .expect("expected state to have no other refs") - .set_output_data(output_data, swap_execution_1_for_2, swap_execution_2_for_1); + .set_output_data(output_data, swap_execution_1_for_2, swap_execution_2_for_1) + .await?; Ok(()) } diff --git a/crates/core/component/dex/src/component/router/tests.rs b/crates/core/component/dex/src/component/router/tests.rs index 226974ba71..81ca29dc37 100644 --- a/crates/core/component/dex/src/component/router/tests.rs +++ b/crates/core/component/dex/src/component/router/tests.rs @@ -8,6 +8,7 @@ use penumbra_num::{fixpoint::U128x128, Amount}; use rand_core::OsRng; use std::sync::Arc; +use crate::component::ValueCircuitBreaker; use crate::lp::SellOrder; use crate::DexParameters; use crate::{ @@ -988,6 +989,19 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_1.clone(), 1u64.into(), 1u64.into()); state_tx.put_position(buy_1).await.unwrap(); + // TODO: later, this should be folded into an open_position method + state_tx + .vcb_credit(Value { + asset_id: gn.id(), + amount: Amount::from(1u64) * gn.unit_amount(), + }) + .await?; + state_tx + .vcb_credit(Value { + asset_id: penumbra.id(), + amount: Amount::from(1u64) * penumbra.unit_amount(), + }) + .await?; state_tx.apply(); // We should be able to call path_search and route through that position. @@ -1015,7 +1029,9 @@ async fn best_position_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) @@ -1061,6 +1077,27 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { let pair_gn_gm = DirectedUnitPair::new(gn.clone(), gm.clone()); let pair_gm_penumbra = DirectedUnitPair::new(gm.clone(), penumbra.clone()); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gm = Value { + asset_id: gm.id(), + amount: Amount::from(100000u128) * gm.unit_amount(), + }; + + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gm).await?; + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + // Create a 2:1 penumbra:gm position (i.e. buy 20 gm at 2 penumbra each). let buy_1 = limit_buy_pq( pair_gm_penumbra.clone(), @@ -1154,7 +1191,9 @@ async fn multi_hop_route_and_fill() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) diff --git a/crates/core/component/dex/src/component/rpc.rs b/crates/core/component/dex/src/component/rpc.rs index 64aaccc2f4..ed3e6914d0 100644 --- a/crates/core/component/dex/src/component/rpc.rs +++ b/crates/core/component/dex/src/component/rpc.rs @@ -23,7 +23,7 @@ use penumbra_proto::{ DomainType, StateReadProto, }; -use crate::ExecutionCircuitBreaker; +use super::ExecutionCircuitBreaker; use crate::{ lp::position::{self, Position}, state_key, DirectedTradingPair, SwapExecution, TradingPair, diff --git a/crates/core/component/dex/src/component/tests.rs b/crates/core/component/dex/src/component/tests.rs index 3f7b727a7a..1aca0cf75b 100644 --- a/crates/core/component/dex/src/component/tests.rs +++ b/crates/core/component/dex/src/component/tests.rs @@ -10,6 +10,7 @@ use rand_core::OsRng; //use crate::TempStorageExt; +use crate::component::ValueCircuitBreaker as _; use crate::lp::action::PositionOpen; use crate::DexParameters; use crate::{ @@ -560,6 +561,21 @@ async fn swap_execution_tests() -> anyhow::Result<()> { let pair_gn_penumbra = DirectedUnitPair::new(gn.clone(), penumbra.clone()); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + // Create a single 1:1 gn:penumbra position (i.e. buy 1 gn at 1 penumbra). let buy_1 = limit_buy(pair_gn_penumbra.clone(), 1u64.into(), 1u64.into()); state_tx.put_position(buy_1).await.unwrap(); @@ -579,7 +595,9 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0, 0, routing_params) @@ -625,6 +643,30 @@ async fn swap_execution_tests() -> anyhow::Result<()> { .get_unit("test_usd") .unwrap(); + // TEMP TODO: disable VCB for this test. Later, remove this code once we restructure + // the position manager. + let infinite_gn = Value { + asset_id: gn.id(), + amount: Amount::from(100000u128) * gn.unit_amount(), + }; + let infinite_gm = Value { + asset_id: gm.id(), + amount: Amount::from(100000u128) * gm.unit_amount(), + }; + let infinite_penumbra = Value { + asset_id: penumbra.id(), + amount: Amount::from(100000u128) * penumbra.unit_amount(), + }; + let infinite_pusd = Value { + asset_id: pusd.id(), + amount: Amount::from(100000u128) * pusd.unit_amount(), + }; + + state_tx.vcb_credit(infinite_gn).await?; + state_tx.vcb_credit(infinite_gm).await?; + state_tx.vcb_credit(infinite_penumbra).await?; + state_tx.vcb_credit(infinite_pusd).await?; + tracing::info!(gm_id = ?gm.id()); tracing::info!(gn_id = ?gn.id()); tracing::info!(pusd_id = ?pusd.id()); @@ -685,7 +727,9 @@ async fn swap_execution_tests() -> anyhow::Result<()> { // Set the batch swap flow for the trading pair. Arc::get_mut(&mut state) .unwrap() - .put_swap_flow(&trading_pair, swap_flow.clone()); + .put_swap_flow(&trading_pair, swap_flow.clone()) + .await + .unwrap(); let routing_params = state.routing_params().await.unwrap(); state .handle_batch_swaps(trading_pair, swap_flow, 0u32.into(), 0, routing_params) diff --git a/crates/core/component/dex/src/event.rs b/crates/core/component/dex/src/event.rs index 8b7c152a8e..660efc6be4 100644 --- a/crates/core/component/dex/src/event.rs +++ b/crates/core/component/dex/src/event.rs @@ -8,6 +8,8 @@ use crate::{ BatchSwapOutputData, SwapExecution, }; +use penumbra_asset::asset; +use penumbra_num::Amount; use penumbra_proto::penumbra::core::component::dex::v1 as pb; pub fn swap(swap: &Swap) -> pb::EventSwap { @@ -91,3 +93,27 @@ pub fn arb_execution(height: u64, swap_execution: SwapExecution) -> pb::EventArb swap_execution: Some(swap_execution.into()), } } + +pub fn vcb_credit( + asset_id: asset::Id, + previous_balance: Amount, + new_balance: Amount, +) -> pb::EventValueCircuitBreakerCredit { + pb::EventValueCircuitBreakerCredit { + asset_id: Some(asset_id.into()), + previous_balance: Some(previous_balance.into()), + new_balance: Some(new_balance.into()), + } +} + +pub fn vcb_debit( + asset_id: asset::Id, + previous_balance: Amount, + new_balance: Amount, +) -> pb::EventValueCircuitBreakerDebit { + pb::EventValueCircuitBreakerDebit { + asset_id: Some(asset_id.into()), + previous_balance: Some(previous_balance.into()), + new_balance: Some(new_balance.into()), + } +} diff --git a/crates/core/component/dex/src/lib.rs b/crates/core/component/dex/src/lib.rs index a7f77e9543..a330e0e971 100644 --- a/crates/core/component/dex/src/lib.rs +++ b/crates/core/component/dex/src/lib.rs @@ -8,13 +8,11 @@ pub mod genesis; pub mod state_key; mod batch_swap_output_data; -mod circuit_breaker; mod params; mod swap_execution; mod trading_pair; pub use batch_swap_output_data::BatchSwapOutputData; -pub(crate) use circuit_breaker::ExecutionCircuitBreaker; pub use params::DexParameters; pub use swap_execution::SwapExecution; pub use trading_pair::{DirectedTradingPair, DirectedUnitPair, TradingPair, TradingPairVar}; diff --git a/crates/core/component/dex/src/lp/position.rs b/crates/core/component/dex/src/lp/position.rs index b31878f2ab..e6aae9c636 100644 --- a/crates/core/component/dex/src/lp/position.rs +++ b/crates/core/component/dex/src/lp/position.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Context}; -use penumbra_asset::asset; +use penumbra_asset::{asset, Value}; use penumbra_num::Amount; use penumbra_proto::{ penumbra::core::component::dex::v1 as pb, serializers::bech32str, DomainType, @@ -156,6 +156,22 @@ impl Position { None } } + + /// Returns the amount of reserves for asset 1. + pub fn reserves_1(&self) -> Value { + Value { + amount: self.reserves.r1, + asset_id: self.phi.pair.asset_1(), + } + } + + /// Returns the amount of reserves for asset 2. + pub fn reserves_2(&self) -> Value { + Value { + amount: self.reserves.r2, + asset_id: self.phi.pair.asset_2(), + } + } } /// A hash of a [`Position`]. diff --git a/crates/core/component/dex/src/state_key.rs b/crates/core/component/dex/src/state_key.rs index cca51976c6..dad76e571e 100644 --- a/crates/core/component/dex/src/state_key.rs +++ b/crates/core/component/dex/src/state_key.rs @@ -1,5 +1,7 @@ use std::string::String; +use penumbra_asset::asset; + use crate::{lp::position, DirectedTradingPair, TradingPair}; pub mod config { @@ -12,6 +14,10 @@ pub mod config { } } +pub fn value_balance(asset_id: &asset::Id) -> String { + format!("dex/value_balance/{asset_id}") +} + pub fn positions(trading_pair: &TradingPair, position_id: &str) -> String { format!("dex/positions/{trading_pair}/opened/{position_id}") } diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs index 9b8568691b..27e6a5c183 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.rs @@ -1444,6 +1444,48 @@ impl ::prost::Name for EventArbExecution { ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) } } +/// Indicates that value was added to the DEX. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerCredit { + /// The asset ID being deposited into the DEX. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the DEX. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the DEX. + #[prost(message, optional, tag = "3")] + pub new_balance: ::core::option::Option, +} +impl ::prost::Name for EventValueCircuitBreakerCredit { + const NAME: &'static str = "EventValueCircuitBreakerCredit"; + const PACKAGE: &'static str = "penumbra.core.component.dex.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) + } +} +/// Indicates that value is leaving the DEX. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerDebit { + /// The asset ID being deposited into the DEX. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the DEX. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the DEX. + #[prost(message, optional, tag = "3")] + pub new_balance: ::core::option::Option, +} +impl ::prost::Name for EventValueCircuitBreakerDebit { + const NAME: &'static str = "EventValueCircuitBreakerDebit"; + const PACKAGE: &'static str = "penumbra.core.component.dex.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("penumbra.core.component.dex.v1.{}", Self::NAME) + } +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DexParameters { diff --git a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs index a9a2c86f01..14e166d4f8 100644 --- a/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.dex.v1.serde.rs @@ -2414,6 +2414,270 @@ impl<'de> serde::Deserialize<'de> for EventSwapClaim { deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventSwapClaim", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for EventValueCircuitBreakerCredit { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.previous_balance.is_some() { + len += 1; + } + if self.new_balance.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.previous_balance.as_ref() { + struct_ser.serialize_field("previousBalance", v)?; + } + if let Some(v) = self.new_balance.as_ref() { + struct_ser.serialize_field("newBalance", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventValueCircuitBreakerCredit { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "previous_balance", + "previousBalance", + "new_balance", + "newBalance", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + PreviousBalance, + NewBalance, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "previousBalance" | "previous_balance" => Ok(GeneratedField::PreviousBalance), + "newBalance" | "new_balance" => Ok(GeneratedField::NewBalance), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventValueCircuitBreakerCredit; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut previous_balance__ = None; + let mut new_balance__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::PreviousBalance => { + if previous_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("previousBalance")); + } + previous_balance__ = map_.next_value()?; + } + GeneratedField::NewBalance => { + if new_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("newBalance")); + } + new_balance__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventValueCircuitBreakerCredit { + asset_id: asset_id__, + previous_balance: previous_balance__, + new_balance: new_balance__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerCredit", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for EventValueCircuitBreakerDebit { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.asset_id.is_some() { + len += 1; + } + if self.previous_balance.is_some() { + len += 1; + } + if self.new_balance.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit", len)?; + if let Some(v) = self.asset_id.as_ref() { + struct_ser.serialize_field("assetId", v)?; + } + if let Some(v) = self.previous_balance.as_ref() { + struct_ser.serialize_field("previousBalance", v)?; + } + if let Some(v) = self.new_balance.as_ref() { + struct_ser.serialize_field("newBalance", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for EventValueCircuitBreakerDebit { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "asset_id", + "assetId", + "previous_balance", + "previousBalance", + "new_balance", + "newBalance", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + AssetId, + PreviousBalance, + NewBalance, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "assetId" | "asset_id" => Ok(GeneratedField::AssetId), + "previousBalance" | "previous_balance" => Ok(GeneratedField::PreviousBalance), + "newBalance" | "new_balance" => Ok(GeneratedField::NewBalance), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = EventValueCircuitBreakerDebit; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut asset_id__ = None; + let mut previous_balance__ = None; + let mut new_balance__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::AssetId => { + if asset_id__.is_some() { + return Err(serde::de::Error::duplicate_field("assetId")); + } + asset_id__ = map_.next_value()?; + } + GeneratedField::PreviousBalance => { + if previous_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("previousBalance")); + } + previous_balance__ = map_.next_value()?; + } + GeneratedField::NewBalance => { + if new_balance__.is_some() { + return Err(serde::de::Error::duplicate_field("newBalance")); + } + new_balance__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(EventValueCircuitBreakerDebit { + asset_id: asset_id__, + previous_balance: previous_balance__, + new_balance: new_balance__, + }) + } + } + deserializer.deserialize_struct("penumbra.core.component.dex.v1.EventValueCircuitBreakerDebit", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for GenesisContent { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index be786d18c9..1800672860 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto index ffe0005c1a..b8468b1e0f 100644 --- a/proto/penumbra/penumbra/core/component/dex/v1/dex.proto +++ b/proto/penumbra/penumbra/core/component/dex/v1/dex.proto @@ -669,6 +669,26 @@ message EventArbExecution { SwapExecution swap_execution = 2; } +// Indicates that value was added to the DEX. +message EventValueCircuitBreakerCredit { + // The asset ID being deposited into the DEX. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the DEX. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the DEX. + num.v1.Amount new_balance = 3; +} + +// Indicates that value is leaving the DEX. +message EventValueCircuitBreakerDebit { + // The asset ID being deposited into the DEX. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the DEX. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the DEX. + num.v1.Amount new_balance = 3; +} + message DexParameters { // Whether or not the DEX is enabled. bool is_enabled = 1;