Skip to content

Commit

Permalink
auction: flesh out dutch auction manager
Browse files Browse the repository at this point in the history
  • Loading branch information
erwanor committed Apr 17, 2024
1 parent 51aeb7e commit 8347506
Showing 1 changed file with 252 additions and 0 deletions.
252 changes: 252 additions & 0 deletions crates/core/component/auction/src/component/dutch_auction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use crate::auction::dutch::{DutchAuction, DutchAuctionDescription, DutchAuctionState};
use crate::auction::AuctionId;
use crate::component::AuctionStoreRead;
use crate::state_key;
use anyhow::Result;
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_num::Amount;
use penumbra_proto::core::component::auction::v1alpha1 as pb;
use penumbra_sct::component::clock::EpochRead;
use prost::{Message, Name};

#[async_trait]
pub(crate) trait DutchAuctionManager: StateWrite {
/// Schedule an auction with the specified [`DutchAuctionDescritpion`].
async fn schedule_auction(&mut self, description: DutchAuctionDescription) {
let DutchAuctionDescription {
input: _,
output_id: _,
max_output: _,
min_output: _,
start_height,
end_height,
step_count,
nonce: _,
} = description;

let next_trigger = Self::compute_next_trigger(TriggerData {
start_height,
end_height,
step_count,
current_height: self
.get_block_height()
.await
.expect("block height is not missing"),
})
.expect("infaillible because of action validation")
.expect("action validation guarantees the auction is not expired");

let state = DutchAuctionState {
sequence: 0,
current_position: None,
next_trigger,
input_reserves: description.input.amount,
output_reserves: Amount::zero(),
};

let dutch_auction = DutchAuction { description, state };

// Set the triggger
// Write position to state
self.write_dutch_auction_state(dutch_auction);
}

/// Terminate a Dutch auction associated with the specified [`AuctionId`].
///
/// # Errors
/// This method returns an error if the id is not found, or if the
/// recorded entry is not of type `DutchAuction`.
async fn close_auction_by_id(&mut self, id: AuctionId) -> Result<()> {
let auction = self
.get_dutch_auction_by_id(id)
.await?
.ok_or_else(|| anyhow::anyhow!("auction not found"))?;
self.close_auction(auction)
}

/// Terminate and update the supplied auction state.
fn close_auction(&mut self, auction_to_close: DutchAuction) -> Result<()> {
let DutchAuctionState {
sequence,
current_position,
next_trigger,
input_reserves,
output_reserves,
} = auction_to_close.state;

// Short-circuit to no-op if the auction is already closed.
if sequence >= 1 {
return Ok(());
}

let auction_id = auction_to_close.description.id();

// We close and retire the DEX position owned by this auction state,
// and return the respective amount of input and output we should credit
// to the total tracked amount, so that it can be returned to its bearer.
let (input_from_position, output_from_position) = if let Some(_position) = current_position
{
// Get position state
// Withdraw position from the dex
// Return reserves so that we can credit them to i/o rs.
(Amount::zero(), Amount::zero())
} else {
(Amount::zero(), Amount::zero())
};

if let Some(_position) = current_position {
// Close position from the dex.
// Withdraw position from the dex.
// Credit balances to total input/output
}

// If a `next_trigger` entry is set, we remove it.
if next_trigger != 0 {
self.unset_trigger_for_id(auction_id, next_trigger)
}

let total_input_reserves = input_reserves + input_from_position;
let total_output_reserves = output_reserves + output_from_position;

let closed_auction = DutchAuction {
description: auction_to_close.description,
state: DutchAuctionState {
sequence: 1u64,
current_position: None,
next_trigger: 0,
input_reserves: total_input_reserves,
output_reserves: total_output_reserves,
},
};
self.write_dutch_auction_state(closed_auction);
Ok(())
}

/// Withdraw a dutch auction, zero-ing out its reserves and increasing its sequence
/// number.
///
/// # Errors
/// This method errors if the auction id is not found, or if the associated
/// entry is not of type [`DutchAuction`].
async fn withdraw_auction_by_id(&mut self, id: AuctionId) -> Result<()> {
let auction = self
.get_dutch_auction_by_id(id)
.await?
.ok_or_else(|| anyhow::anyhow!("auction not found"))?;
self.withdraw_auction(auction);
Ok(())
}

fn withdraw_auction(&mut self, mut auction: DutchAuction) {
auction.state.sequence = auction.state.sequence.saturating_add(1);
auction.state.current_position = None;
auction.state.input_reserves = Amount::zero();
auction.state.output_reserves = Amount::zero();
self.write_dutch_auction_state(auction)
}
}

impl<T: StateWrite + ?Sized> DutchAuctionManager for T {}

trait Inner: StateWrite {
/// Serialize a `DutchAuction` as an `Any` into chain state.
fn write_dutch_auction_state(&mut self, new_state: DutchAuction) {
let id = new_state.description.id();
let key = state_key::auction_store::by_id(id);
let pb_state: pb::DutchAuction = new_state.into();
let raw_auction = pb_state.encode_length_delimited_to_vec();

let any_auction = prost_types::Any {
type_url: pb::DutchAuction::full_name(),
value: raw_auction,
};

let raw_any = any_auction.encode_length_delimited_to_vec();

self.put_raw(key, raw_any);
}

/// Compute the next trigger height, return `None` if the step count
/// has been reached and the auction should be retired.
///
/// # Errors
/// This method errors if the block interval is not a multiple of the
/// specified `step_count`, or if it operates over an invalid block
/// interval (which should NEVER happen unless validation is broken).
///
// TODO(erwan): doing everything checked at least for now, will remove as
// i fill the tests module.
fn compute_next_trigger(trigger_data: TriggerData) -> Result<Option<u64>> {
let block_interval = trigger_data
.end_height
.checked_sub(trigger_data.start_height)
.ok_or_else(|| {
anyhow::anyhow!(
"block interval calculation has underflowed (end={}, start={})",
trigger_data.end_height,
trigger_data.start_height
)
})?;

// Compute the step size, based on the block interval and the number of
// discrete steps the auction specifies.
let step_size = block_interval
.checked_div(trigger_data.step_count)
.ok_or_else(|| anyhow::anyhow!("step count is zero"))?;

// Compute the step index for the current height, this should work even if
// the supplied height does not fall perfectly on a step boundary. First, we
// "clamp it" to a previous step index, then we increment by 1 to compute the
// next one, and finally we determine a concrete trigger height based off that.
let prev_step_index = trigger_data
.current_height
.checked_div(step_size)
.ok_or_else(|| anyhow::anyhow!("step size is zero"))?;

if prev_step_index >= trigger_data.step_count {
return Ok(None);
}

let next_step_index = prev_step_index
.checked_add(1)
.ok_or_else(|| anyhow::anyhow!("step index has overflowed"))?;

let next_step_size_from_start =
step_size.checked_mul(next_step_index).ok_or_else(|| {
anyhow::anyhow!(
"next step size from start has overflowed (step_size={}, next_step_index={})",
step_size,
next_step_index
)
})?;

Ok(trigger_data
.start_height
.checked_add(next_step_size_from_start))
}

/// Set the trigger for an auction.
fn set_trigger_for_id(&mut self, auction_id: AuctionId, trigger_height: u64) {
let trigger_path = state_key::dutch::trigger::auction_at_height(auction_id, trigger_height);
self.put_raw(trigger_path, vec![]);
}

/// Delete the trigger for an auction and height
fn unset_trigger_for_id(&mut self, auction_id: AuctionId, trigger_height: u64) {
let trigger_path = state_key::dutch::trigger::auction_at_height(auction_id, trigger_height);
self.delete(trigger_path);
}
}

impl<T: StateWrite + ?Sized> Inner for T {}

struct TriggerData {
pub start_height: u64,
pub end_height: u64,
pub current_height: u64,
pub step_count: u64,
}

#[cfg(tests)]
mod tests {}

0 comments on commit 8347506

Please sign in to comment.