From a44f05b3819b96bb177ba8b4f60b696e4c0a07a2 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 14 Oct 2021 15:28:18 +1100 Subject: [PATCH 1/2] Lock in profit on collaborative close price Once we have collaborative close present in the state(s) we lock in the profit based on the price of the collaborative close. Note: Currently no automated validation on the maker side if the given amounts and price of collaborative settlement are sane. --- daemon/src/maker_cfd.rs | 14 ++++++++---- daemon/src/model/cfd.rs | 50 +++++++++++++++++++++++------------------ daemon/src/monitor.rs | 4 ++-- daemon/src/taker_cfd.rs | 7 +++--- daemon/src/wire.rs | 1 + 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/daemon/src/maker_cfd.rs b/daemon/src/maker_cfd.rs index 9fd9007..15f9860 100644 --- a/daemon/src/maker_cfd.rs +++ b/daemon/src/maker_cfd.rs @@ -4,8 +4,8 @@ use crate::db::{ }; use crate::maker_inc_connections::TakerCommand; use crate::model::cfd::{ - Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, - Role, RollOverProposal, SettlementKind, SettlementProposal, TimestampedTransaction, + Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, CollaborativeSettlement, Dlc, + Order, OrderId, Origin, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, }; use crate::model::{TakerId, Usd}; @@ -304,7 +304,11 @@ impl Actor { let (tx, sig_maker) = dlc.close_transaction(proposal)?; cfd.handle(CfdStateChangeEvent::ProposalSigned( - TimestampedTransaction::new(tx.clone(), dlc.script_pubkey_for(cfd.role())), + CollaborativeSettlement::new( + tx.clone(), + dlc.script_pubkey_for(cfd.role()), + proposal.price, + ), ))?; insert_new_cfd_state_by_order_id(cfd.order.id, &cfd.state, &mut conn).await?; self.cfd_feed_actor_inbox @@ -1077,6 +1081,7 @@ impl Handler for Actor { timestamp, taker, maker, + price, } => { log_error!(self.handle_propose_settlement( taker_id, @@ -1084,7 +1089,8 @@ impl Handler for Actor { order_id, timestamp, taker, - maker + maker, + price } )) } diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 79e350b..e7d6bf7 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -217,7 +217,7 @@ pub enum CfdState { common: CfdStateCommon, dlc: Dlc, attestation: Option, - collaborative_close: Option, + collaborative_close: Option, }, /// The commit transaction was published but it not final yet @@ -289,7 +289,7 @@ pub enum CfdState { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Payout { - CollaborativeClose(TimestampedTransaction), + CollaborativeClose(CollaborativeSettlement), Cet(Attestation), } @@ -407,7 +407,7 @@ impl CfdState { self.get_common().transition_timestamp } - pub fn get_collaborative_close(&self) -> Option { + pub fn get_collaborative_close(&self) -> Option { match self { CfdState::Open { collaborative_close, @@ -486,6 +486,7 @@ pub struct SettlementProposal { pub timestamp: SystemTime, pub taker: Amount, pub maker: Amount, + pub price: Usd, } /// Proposed collaborative settlement @@ -547,16 +548,17 @@ impl Cfd { pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { // TODO: We should use the payout curve here and not just the current price! - // TODO: Use the collab settlement if there was one - let current_price = if let Some(attestation) = self.attestation() { - attestation.price() - } else { - current_price + + let closing_price = match (self.attestation(), self.collaborative_close()) { + (Some(_attestation), Some(collaborative_close)) => collaborative_close.price, + (None, Some(collaborative_close)) => collaborative_close.price, + (Some(attestation), None) => attestation.price(), + (None, None) => current_price, }; let (p_n_l, p_n_l_percent) = calculate_profit( self.order.price, - current_price, + closing_price, self.quantity_usd, self.margin()?, self.position(), @@ -569,18 +571,20 @@ impl Cfd { let payout_curve = payout_curve::calculate(self.order.price, self.quantity_usd, self.order.leverage)?; - let current_price = current_price.try_into_u64()?; - - let payout = payout_curve - .iter() - .find(|&x| x.digits().range().contains(¤t_price)) - .context("find current price on the payout curve")?; + let payout = { + let current_price = current_price.try_into_u64()?; + payout_curve + .iter() + .find(|&x| x.digits().range().contains(¤t_price)) + .context("find current price on the payout curve")? + }; let settlement = SettlementProposal { order_id: self.order.id, timestamp: SystemTime::now(), taker: *payout.taker_amount(), maker: *payout.maker_amount(), + price: current_price, }; Ok(settlement) @@ -1168,7 +1172,7 @@ impl Cfd { } } - pub fn collaborative_close(&self) -> Option { + pub fn collaborative_close(&self) -> Option { match self.state.clone() { CfdState::Open { collaborative_close: Some(collaborative_close), @@ -1235,13 +1239,13 @@ pub enum CfdStateChangeEvent { OracleAttestation(Attestation), CetSent, /// Settlement proposal was signed by the taker and sent to the maker - ProposalSigned(TimestampedTransaction), + ProposalSigned(CollaborativeSettlement), } /// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin fn calculate_profit( initial_price: Usd, - current_price: Usd, + closing_price: Usd, quantity: Usd, margin: Amount, position: Position, @@ -1253,7 +1257,7 @@ fn calculate_profit( .checked_div(initial_price.0) .context("Calculating inverse of initial_price resulted in overflow")?; let current_price_inverse = dec!(1) - .checked_div(current_price.0) + .checked_div(closing_price.0) .context("Calculating inverse of current_price resulted in overflow")?; // calculate profit/loss (P and L) in BTC @@ -1710,15 +1714,16 @@ pub struct RevokedCommit { /// timestamp as it could have occured for different reasons (like a new /// attestation in Open state) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct TimestampedTransaction { +pub struct CollaborativeSettlement { pub tx: Transaction, pub timestamp: SystemTime, #[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")] payout: Amount, + price: Usd, } -impl TimestampedTransaction { - pub fn new(tx: Transaction, own_script_pubkey: Script) -> Self { +impl CollaborativeSettlement { + pub fn new(tx: Transaction, own_script_pubkey: Script, price: Usd) -> Self { // Falls back to Amount::ZERO in case we don't find an output that matches out script pubkey // The assumption is, that this can happen for cases where we were liuqidated let payout = match tx @@ -1740,6 +1745,7 @@ impl TimestampedTransaction { tx, timestamp: SystemTime::now(), payout, + price, } } diff --git a/daemon/src/monitor.rs b/daemon/src/monitor.rs index 033e603..c47c54d 100644 --- a/daemon/src/monitor.rs +++ b/daemon/src/monitor.rs @@ -1,4 +1,4 @@ -use crate::model::cfd::{CetStatus, Cfd, CfdState, Dlc, OrderId, TimestampedTransaction}; +use crate::model::cfd::{CetStatus, Cfd, CfdState, CollaborativeSettlement, Dlc, OrderId}; use crate::model::BitMexPriceEventId; use crate::oracle::Attestation; use crate::{log_error, model, oracle}; @@ -84,7 +84,7 @@ impl Actor { actor.monitor_commit_refund_timelock(¶ms, cfd.order.id); actor.monitor_refund_finality(¶ms,cfd.order.id); - if let Some(TimestampedTransaction { tx, ..} + if let Some(CollaborativeSettlement { tx, ..} ) = cfd.state.get_collaborative_close() { let close_params = (tx.txid(), tx.output.first().expect("have output").script_pubkey.clone()); diff --git a/daemon/src/taker_cfd.rs b/daemon/src/taker_cfd.rs index 0e4b66f..c0df59c 100644 --- a/daemon/src/taker_cfd.rs +++ b/daemon/src/taker_cfd.rs @@ -3,8 +3,8 @@ use crate::db::{ load_cfd_by_order_id, load_cfds_by_oracle_event_id, load_order_by_id, }; use crate::model::cfd::{ - Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, Dlc, Order, OrderId, Origin, - Role, RollOverProposal, SettlementKind, SettlementProposal, TimestampedTransaction, + Attestation, Cfd, CfdState, CfdStateChangeEvent, CfdStateCommon, CollaborativeSettlement, Dlc, + Order, OrderId, Origin, Role, RollOverProposal, SettlementKind, SettlementProposal, UpdateCfdProposal, UpdateCfdProposals, }; use crate::model::{BitMexPriceEventId, Usd}; @@ -204,6 +204,7 @@ impl Actor { timestamp: proposal.timestamp, taker: proposal.taker, maker: proposal.maker, + price: proposal.price, }) .await?; Ok(()) @@ -370,7 +371,7 @@ impl Actor { .await?; cfd.handle(CfdStateChangeEvent::ProposalSigned( - TimestampedTransaction::new(tx, dlc.script_pubkey_for(cfd.role())), + CollaborativeSettlement::new(tx, dlc.script_pubkey_for(cfd.role()), proposal.price), ))?; insert_new_cfd_state_by_order_id(cfd.order.id, &cfd.state, &mut conn).await?; diff --git a/daemon/src/wire.rs b/daemon/src/wire.rs index 60d3550..774b8d0 100644 --- a/daemon/src/wire.rs +++ b/daemon/src/wire.rs @@ -31,6 +31,7 @@ pub enum TakerToMaker { taker: Amount, #[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")] maker: Amount, + price: Usd, }, InitiateSettlement { order_id: OrderId, From dce6eb3c3d6629e5908d9f129cc976b25ee66d0b Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 14 Oct 2021 16:53:50 +1100 Subject: [PATCH 2/2] Remove Todo because we already have a ticket Todo already tracked here: https://github.com/comit-network/hermes/issues/278 --- daemon/src/model/cfd.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index e7d6bf7..996314e 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -547,8 +547,6 @@ impl Cfd { } pub fn profit(&self, current_price: Usd) -> Result<(SignedAmount, Percent)> { - // TODO: We should use the payout curve here and not just the current price! - let closing_price = match (self.attestation(), self.collaborative_close()) { (Some(_attestation), Some(collaborative_close)) => collaborative_close.price, (None, Some(collaborative_close)) => collaborative_close.price,