From 9a53f332c96e16b38a17393a82bd85d575e4ebd7 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 9 May 2024 09:42:30 -0400 Subject: [PATCH 1/5] protos(auction): add value credit/debit event messages --- .../component/auction/v1alpha1/auction.proto | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto index ee9958c236..f93720a898 100755 --- a/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto +++ b/proto/penumbra/penumbra/core/component/auction/v1alpha1/auction.proto @@ -200,3 +200,23 @@ message EventDutchAuctionEnded { DutchAuctionState state = 2; Reason reason = 3; } + +// A message emitted when value flows *into* the auction component. +message EventValueCircuitBreakerCredit { + // The asset ID being deposited into the Auction component. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the Auction component. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the Auction component. + num.v1.Amount new_balance = 3; +} + +// A message emitted when value flows *out* of the auction component. +message EventValueCircuitBreakerDebit { + // The asset ID being deposited into the Auction component. + asset.v1.AssetId asset_id = 1; + // The previous balance of the asset in the Auction component. + num.v1.Amount previous_balance = 2; + // The new balance of the asset in the Auction component. + num.v1.Amount new_balance = 3; +} From 96d6f4e756d423bdf494e2a5ab37065c69a1b723 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 9 May 2024 09:42:52 -0400 Subject: [PATCH 2/5] penumbra: regenerate protos --- ...enumbra.core.component.auction.v1alpha1.rs | 46 +++ ...a.core.component.auction.v1alpha1.serde.rs | 264 ++++++++++++++++++ .../proto/src/gen/proto_descriptor.bin.no_lfs | Bin 409171 -> 410540 bytes 3 files changed, 310 insertions(+) diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs index 563cece891..1effa724c2 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.rs @@ -475,6 +475,52 @@ impl ::prost::Name for EventDutchAuctionEnded { ) } } +/// A message emitted when value flows *into* the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerCredit { + /// The asset ID being deposited into the Auction component. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the Auction component. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the Auction component. + #[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.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} +/// A message emitted when value flows *out* of the auction component. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EventValueCircuitBreakerDebit { + /// The asset ID being deposited into the Auction component. + #[prost(message, optional, tag = "1")] + pub asset_id: ::core::option::Option, + /// The previous balance of the asset in the Auction component. + #[prost(message, optional, tag = "2")] + pub previous_balance: ::core::option::Option, + /// The new balance of the asset in the Auction component. + #[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.auction.v1alpha1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!( + "penumbra.core.component.auction.v1alpha1.{}", Self::NAME + ) + } +} /// Generated client implementations. #[cfg(feature = "rpc")] pub mod query_service_client { diff --git a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs index fbfdc982da..d495800b26 100644 --- a/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.auction.v1alpha1.serde.rs @@ -2404,6 +2404,270 @@ impl<'de> serde::Deserialize<'de> for EventDutchAuctionUpdated { deserializer.deserialize_struct("penumbra.core.component.auction.v1alpha1.EventDutchAuctionUpdated", 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.auction.v1alpha1.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.auction.v1alpha1.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.auction.v1alpha1.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.auction.v1alpha1.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.auction.v1alpha1.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.auction.v1alpha1.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 78f7ce73c8e6f167ce0146aceb773a4068e3fe49..91d3694fa137f3158b67aef35c545215968e852d 100644 GIT binary patch delta 708 zcma)&u}dRC9EJ0nokW>KjB6I+p6Hxs3lCi!7HO|N*!N7Lr~LCddiwHoLg#tMa^^UsDuof-;Q>3?P&W!~m*N zSS?}zRlP;?&}8VLJAytkzr0J6U2qc8PX85U0DecUF<+G7iK@LV3LR#lY=}a{%bF5} zh*>oy%J4Qnye$eHKqwpkhv?sJib4eFL5VULMAiQjHR_2%2M|gH@;+KuqR;|V$D$vh gwGG>`hr~sjm~iKQY0+Z5L^upb!{L>9DL8oi2c;y6GXMYp delta 87 zcmZ3}E_wNnM8g(Fu4JZv(bMnyGD}U*InOLN`Ff?)v_eLK?Rm+JllZrvEn)O$>30kK#g)&Kwi From 2400db0bf4338510d3ac2645e60347a9f640f6f5 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 9 May 2024 10:49:57 -0400 Subject: [PATCH 3/5] auction(event): add value balance protoevents --- crates/core/component/auction/src/event.rs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/core/component/auction/src/event.rs b/crates/core/component/auction/src/event.rs index 6343cde465..f3942f4286 100644 --- a/crates/core/component/auction/src/event.rs +++ b/crates/core/component/auction/src/event.rs @@ -1,5 +1,7 @@ use crate::auction::dutch::{DutchAuctionDescription, DutchAuctionState}; use crate::auction::AuctionId; +use penumbra_asset::asset; +use penumbra_num::Amount; use penumbra_proto::penumbra::core::component::auction::v1alpha1 as pb; /// Event for a Dutch auction that has been scheduled. @@ -59,3 +61,29 @@ pub fn dutch_auction_exhausted( reason: pb::event_dutch_auction_ended::Reason::Filled as i32, } } + +// Event for value flowing *into* the auction component. +pub fn auction_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()), + } +} + +// Event for value flowing *out of* the auction component. +pub fn auction_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()), + } +} From 2a0894dacbba370021cee9433d273af304d38990 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 9 May 2024 11:12:34 -0400 Subject: [PATCH 4/5] auction(vcb): implement `AuctionCircuitBreaker` --- .../auction/src/component/auction.rs | 70 ++++++++++++++++++- .../core/component/auction/src/state_key.rs | 14 ++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/core/component/auction/src/component/auction.rs b/crates/core/component/auction/src/component/auction.rs index 7a7e215431..8213c77641 100644 --- a/crates/core/component/auction/src/component/auction.rs +++ b/crates/core/component/auction/src/component/auction.rs @@ -1,8 +1,12 @@ use crate::component::dutch_auction::HandleDutchTriggers; +use crate::event; use anyhow::Result; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use cnidarium_component::Component; +use penumbra_asset::asset; +use penumbra_asset::Value; +use penumbra_num::Amount; use penumbra_proto::StateReadProto; use penumbra_proto::StateWriteProto; use std::sync::Arc; @@ -15,8 +19,6 @@ pub struct Auction {} #[async_trait] impl Component for Auction { - // Note: this is currently empty, but will make future - // addition easy to do. type AppState = crate::genesis::Content; #[instrument(name = "auction", skip(state, app_state))] @@ -81,5 +83,69 @@ pub trait StateWriteExt: StateWrite { impl StateWriteExt for T {} +/// Internal trait implementing value flow tracking. +pub(crate) trait AuctionCircuitBreaker: StateWrite { + /// Fetch the current balance of the auction circuit breaker for a given asset, + /// returning zero if no balance is tracked yet. + async fn get_auction_value_balance_for(&self, asset_id: &asset::Id) -> Amount { + self.get(&state_key::value_balance::for_asset(asset_id)) + .await + .expect("failed to fetch auction value breaker balance") + .unwrap_or_else(|| Amount::zero()) + } + + /// Credit a deposit into the auction component. + async fn auction_vcb_credit(&mut self, value: Value) -> Result<()> { + let prev_balance = self.get_auction_value_balance_for(&value.asset_id).await; + let new_balance = prev_balance.checked_add(&value.amount).ok_or_else(|| { + tracing::error!( + ?prev_balance, + ?value, + "overflowed balance while crediting auction circuit breaker" + ); + anyhow::anyhow!("overflowed balance while crediting auction circuit breaker") + })?; + + // Write the new balance to the chain state. + self.put( + state_key::value_balance::for_asset(&value.asset_id), + new_balance, + ); + // And emit an event to trace the value flow. + self.record_proto(event::auction_vcb_credit( + value.asset_id, + prev_balance, + new_balance, + )); + Ok(()) + } + + /// Debit a balance from the auction component. + async fn auction_vcb_debit(&mut self, value: Value) -> Result<()> { + let prev_balance = self.get_auction_value_balance_for(&value.asset_id).await; + let new_balance = prev_balance.checked_sub(&value.amount).ok_or_else(|| { + tracing::error!( + ?prev_balance, + ?value, + "underflowed balance while debiting auction circuit breaker" + ); + anyhow::anyhow!("underflowed balance while debiting auction circuit breaker") + })?; + + // Write the new balance to the chain state. + self.put( + state_key::value_balance::for_asset(&value.asset_id), + new_balance, + ); + // And emit an event to trace the value flow out of the component. + self.record_proto(event::auction_vcb_debit( + value.asset_id, + prev_balance, + new_balance, + )); + Ok(()) + } +} + #[cfg(tests)] mod tests {} diff --git a/crates/core/component/auction/src/state_key.rs b/crates/core/component/auction/src/state_key.rs index d34a3edf86..908a7dbf6e 100644 --- a/crates/core/component/auction/src/state_key.rs +++ b/crates/core/component/auction/src/state_key.rs @@ -8,6 +8,20 @@ pub mod parameters { } } +pub(crate) mod value_balance { + use penumbra_asset::asset; + + #[allow(dead_code)] // For some reason, this shows up as unused + pub(crate) fn prefix() -> &'static str { + "auction/value_breaker/" + } + + #[allow(dead_code)] // For some reason, this shows up as unused + pub(crate) fn for_asset(asset_id: &asset::Id) -> String { + format!("{}{asset_id}", prefix()) + } +} + pub mod auction_store { use crate::auction::id::AuctionId; From 1d2e4e0e7d89bb17d6fc1d5fdf9b942ae9e134e3 Mon Sep 17 00:00:00 2001 From: Erwan Or Date: Thu, 9 May 2024 12:10:08 -0400 Subject: [PATCH 5/5] auction(vcb): track value inflows/outflows --- .../action_handler/dutch/schedule.rs | 2 +- .../action_handler/dutch/withdraw.rs | 2 +- .../auction/src/component/auction.rs | 39 +++++++- .../auction/src/component/dutch_auction.rs | 97 ++++++++++++++----- .../component/auction/src/component/mod.rs | 1 + 5 files changed, 116 insertions(+), 25 deletions(-) diff --git a/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs b/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs index 34566dd41e..2652a0e7f6 100644 --- a/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs +++ b/crates/core/component/auction/src/component/action_handler/dutch/schedule.rs @@ -126,7 +126,7 @@ impl ActionHandler for ActionDutchAuctionSchedule { "the supplied auction id is already known to the chain (id={id})" ); - state.schedule_auction(schedule.description.clone()).await; + state.schedule_auction(schedule.description.clone()).await?; Ok(()) } } diff --git a/crates/core/component/auction/src/component/action_handler/dutch/withdraw.rs b/crates/core/component/auction/src/component/action_handler/dutch/withdraw.rs index 4492875cd7..adb0972336 100644 --- a/crates/core/component/auction/src/component/action_handler/dutch/withdraw.rs +++ b/crates/core/component/auction/src/component/action_handler/dutch/withdraw.rs @@ -49,7 +49,7 @@ impl ActionHandler for ActionDutchAuctionWithdraw { // Execute the withdrawal, zero-ing out the auction state // and increasing its sequence number. - let withdrawn_balance = state.withdraw_auction(auction_state); + let withdrawn_balance = state.withdraw_auction(auction_state).await?; // Check that the reported balance commitment, match the recorded reserves. let expected_reserve_commitment = withdrawn_balance.commit(Fr::zero()); diff --git a/crates/core/component/auction/src/component/auction.rs b/crates/core/component/auction/src/component/auction.rs index 8213c77641..04457cb42f 100644 --- a/crates/core/component/auction/src/component/auction.rs +++ b/crates/core/component/auction/src/component/auction.rs @@ -84,6 +84,41 @@ pub trait StateWriteExt: StateWrite { impl StateWriteExt for T {} /// Internal trait implementing value flow tracking. +/// # Overview +/// +/// +/// ║ +/// ║ +/// User initiated +/// Auction ║ +/// component ║ +/// ┏━━━━━━━━━━━┓ ▼ +/// ┃┌─────────┐┃ ╔════════════════════════╗ +/// ┃│ A │┃◀━━value in━━━━━━━━━━║ Schedule auction A ║ +/// ┃└─────────┘┃ ╚════════════════════════╝ +/// ┃ ┃ +/// ┃ │ ┃ +/// ┃ ┃ ■■■■■■■■■■■■■■■■■■■■■■■■■■ +/// ┃ closed ┃━━━value out by ━━━━▶■■■■■■Dex□black□box■■■■■■■ +/// ┃ then ┃ creating lp ■■■■■■■■■■■■■■■■■■■■■■■■■■ +/// ┃withdrawn ┃ ┃ +/// ┃ ┃ ┃ +/// ┃ │ ┃ value in by ┃ +/// ┃ ┃◀━━━━━━━━━━━━━━━withdrawing lp━━━━┛ +/// ┃ │ ┃ +/// ┃ ▼ ┃ +/// ┃┌ ─ ─ ─ ─ ┐┃ ╔════════════════════════╗ +/// ┃ A ┃━━━value out━━━━━━━━▶║ Withdraw auction A ║ +/// ┃└ ─ ─ ─ ─ ┘┃ ╚════════════════════════╝ +/// ┗━━━━━━━━━━━┛ ▲ +/// ║ +/// ║ +/// User initiated +/// withdrawl +/// ║ +/// ║ +/// ║ +/// pub(crate) trait AuctionCircuitBreaker: StateWrite { /// Fetch the current balance of the auction circuit breaker for a given asset, /// returning zero if no balance is tracked yet. @@ -91,7 +126,7 @@ pub(crate) trait AuctionCircuitBreaker: StateWrite { self.get(&state_key::value_balance::for_asset(asset_id)) .await .expect("failed to fetch auction value breaker balance") - .unwrap_or_else(|| Amount::zero()) + .unwrap_or_else(Amount::zero) } /// Credit a deposit into the auction component. @@ -147,5 +182,7 @@ pub(crate) trait AuctionCircuitBreaker: StateWrite { } } +impl AuctionCircuitBreaker for T {} + #[cfg(tests)] mod tests {} diff --git a/crates/core/component/auction/src/component/dutch_auction.rs b/crates/core/component/auction/src/component/dutch_auction.rs index 194757f293..33a0307702 100644 --- a/crates/core/component/auction/src/component/dutch_auction.rs +++ b/crates/core/component/auction/src/component/dutch_auction.rs @@ -4,9 +4,10 @@ use std::pin::Pin; use crate::auction::dutch::{DutchAuction, DutchAuctionDescription, DutchAuctionState}; use crate::auction::AuctionId; use crate::component::trigger_data::TriggerData; +use crate::component::AuctionCircuitBreaker; use crate::component::AuctionStoreRead; use crate::{event, state_key}; -use anyhow::Result; +use anyhow::{Context, Result}; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; use futures::StreamExt; @@ -25,7 +26,7 @@ use prost::{Message, Name}; pub(crate) trait DutchAuctionManager: StateWrite { /// Schedule an auction for the specified [`DutchAuctionDescritpion`], initializing /// its state, and registering it for execution by the component. - async fn schedule_auction(&mut self, description: DutchAuctionDescription) { + async fn schedule_auction(&mut self, description: DutchAuctionDescription) -> Result<()> { let auction_id = description.id(); let DutchAuctionDescription { input: _, @@ -66,12 +67,17 @@ pub(crate) trait DutchAuctionManager: StateWrite { state, }; + // Deposit into the component's value balance. + self.auction_vcb_credit(dutch_auction.description.input) + .await + .context("failed to schedule auction")?; // Set the triggger self.set_trigger_for_dutch_id(auction_id, next_trigger); // Write position to state self.write_dutch_auction_state(dutch_auction); // Emit an event self.record_proto(event::dutch_auction_schedule_event(auction_id, description)); + Ok(()) } /// Execute the [`DutchAuction`] associated with [`AuctionId`], ticking its @@ -156,21 +162,52 @@ pub(crate) trait DutchAuctionManager: StateWrite { state: old_dutch_auction.state, }; - // After consuming the LP, we reset the state, getting ready to either - // execute another session, or retire the auction. + // First, we reset the state (Lp/trigger tracking), transfer value from the dex + // and prepare to either: execute another session, or retire the auction altogether. + + // 1. We untrack the old position. new_dutch_auction.state.current_position = None; - new_dutch_auction.state.input_reserves += lp_reserves - .provided() - .filter(|v| v.asset_id == input.asset_id) - .map(|v| v.amount) - .sum::(); - new_dutch_auction.state.output_reserves += lp_reserves - .provided() - .filter(|v| v.asset_id == output_id) - .map(|v| v.amount) - .sum::(); + // 2. We untrack the trigger. new_dutch_auction.state.next_trigger = None; + /* *********** value transfer *************** */ + // Critically, we need to orchestrate a value transfer from the Dex (lp position) + // into the auction component. This is done in three steps: + // 1. Compute the LP inflow to the auction's input and output reserves + // 2. Credit the auction's value balance with the respective inflows. + // 3. Add the inflows to the auction's reserves. + + // 1. We compute the inflow from the LP's reserves. + let lp_inflow_input_asset = Value { + asset_id: auction_input_id, + amount: lp_reserves + .provided() + .filter(|v| v.asset_id == auction_input_id) + .map(|v| v.amount) + .sum::(), + }; + let lp_inflow_output_asset = Value { + asset_id: auction_output_id, + amount: lp_reserves + .provided() + .filter(|v| v.asset_id == auction_output_id) + .map(|v| v.amount) + .sum::(), + }; + + // 2. We credit the auction's value balance with the inflows. + self.auction_vcb_credit(lp_inflow_input_asset) + .await + .context("failed to absorb LP inflow of input asset into auction value balance")?; + self.auction_vcb_credit(lp_inflow_output_asset) + .await + .context("failed to absorb LP inflow of output asset into auction value balance")?; + + // 3. We add the inflows to the auction's reserves. + new_dutch_auction.state.input_reserves += lp_inflow_input_asset.amount; + new_dutch_auction.state.output_reserves += lp_inflow_output_asset.amount; + /* ***************** end value transfer ************************** */ + // Compute the current step index, between 0 and `step_count`. let step_index = auction_trigger .compute_step_index(trigger_height) @@ -316,21 +353,31 @@ pub(crate) trait DutchAuctionManager: StateWrite { .get_dutch_auction_by_id(id) .await? .ok_or_else(|| anyhow::anyhow!("auction not found"))?; - self.withdraw_auction(auction); + self.withdraw_auction(auction).await?; Ok(()) } - fn withdraw_auction(&mut self, mut auction: DutchAuction) -> Balance { - let previous_input_reserves = Balance::from(Value { + async fn withdraw_auction(&mut self, mut auction: DutchAuction) -> Result { + let previous_input_reserves = Value { amount: auction.state.input_reserves, asset_id: auction.description.input.asset_id, - }); - let previous_output_reserves = Balance::from(Value { + }; + let previous_output_reserves = Value { amount: auction.state.output_reserves, asset_id: auction.description.output_id, - }); + }; + + // We debit the auction's value balance with the outflows, aborting + // if the balance underflows. + self.auction_vcb_debit(previous_input_reserves) + .await + .context("couldn't withdraw input reserves from auction")?; + self.auction_vcb_debit(previous_output_reserves) + .await + .context("couldn't withdraw output reserves from auction")?; - let withdraw_balance = previous_input_reserves + previous_output_reserves; + let withdraw_balance = + Balance::from(previous_input_reserves) + Balance::from(previous_output_reserves); auction.state.sequence = auction.state.sequence.saturating_add(1); auction.state.current_position = None; @@ -339,7 +386,7 @@ pub(crate) trait DutchAuctionManager: StateWrite { auction.state.output_reserves = Amount::zero(); self.write_dutch_auction_state(auction); - withdraw_balance + Ok(withdraw_balance) } } @@ -450,6 +497,12 @@ trait Inner: StateWrite { attempt_counter += 1; continue; } else { + self.auction_vcb_debit(lp.reserves_1()) + .await + .context("failed to debit vcb of r1 during position allocation")?; + self.auction_vcb_debit(lp.reserves_2()) + .await + .context("failed to debit vcb of r2 during position allocation")?; self.open_position(lp).await.expect("no state incoherence"); return Ok(position_id); } diff --git a/crates/core/component/auction/src/component/mod.rs b/crates/core/component/auction/src/component/mod.rs index f669f55bbb..38cd818bef 100644 --- a/crates/core/component/auction/src/component/mod.rs +++ b/crates/core/component/auction/src/component/mod.rs @@ -7,6 +7,7 @@ pub mod rpc; mod trigger_data; pub use auction::Auction; +pub(crate) use auction::AuctionCircuitBreaker; pub use auction::{StateReadExt, StateWriteExt}; pub(crate) use auction_store::AuctionStoreRead; pub(crate) use dutch_auction::DutchAuctionManager;