From fceb18d98b458eec73b6953960d845f56b75ea24 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 16 Sep 2021 17:32:14 +1000 Subject: [PATCH] Use actual margin value for contract setup Includes validating that the amount the other party commits to is the expected amount based on calculating the counterparty margin out of the CFD values. Note: We could also remove the lock amount from the messages and use the calculation on both sides - that would simplify things. --- daemon/sqlx-data.json | 96 +++++++++++++++++++++++++++ daemon/src/db.rs | 102 +++++++++++++++++++++++++++++ daemon/src/maker_cfd_actor.rs | 14 ++-- daemon/src/model/cfd.rs | 11 ++++ daemon/src/setup_contract_actor.rs | 10 ++- daemon/src/taker_cfd_actor.rs | 13 ++-- 6 files changed, 228 insertions(+), 18 deletions(-) diff --git a/daemon/sqlx-data.json b/daemon/sqlx-data.json index 9aab65e..dc97e7a 100644 --- a/daemon/sqlx-data.json +++ b/daemon/sqlx-data.json @@ -152,6 +152,102 @@ "nullable": [] } }, + "a8454a2e4288b58d625f10722c6e23a51d5d17e6bb7af9685bcdbe64546969fa": { + "query": "\n select\n cfds.id as cfd_id,\n orders.uuid as order_id,\n orders.initial_price as price,\n orders.min_quantity as min_quantity,\n orders.max_quantity as max_quantity,\n orders.leverage as leverage,\n orders.trading_pair as trading_pair,\n orders.position as position,\n orders.origin as origin,\n orders.liquidation_price as liquidation_price,\n orders.creation_timestamp as creation_timestamp,\n orders.term as term,\n cfds.quantity_usd as quantity_usd,\n cfd_states.state as state\n from cfds as cfds\n inner join orders as orders on cfds.order_uuid = ?\n inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id\n where cfd_states.state in (\n select\n state\n from cfd_states\n where cfd_id = cfds.id\n order by id desc\n limit 1\n )\n ", + "describe": { + "columns": [ + { + "name": "cfd_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "order_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "price", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "min_quantity", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "max_quantity", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "leverage", + "ordinal": 5, + "type_info": "Int64" + }, + { + "name": "trading_pair", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "position", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "origin", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "liquidation_price", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "creation_timestamp", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "term", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "quantity_usd", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 13, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + } + }, "f3dce76f316212c91cb3402b0cef00f1c9adbef8519c54e9bdbd373aab946209": { "query": "\n select * from orders where uuid = ?;\n ", "describe": { diff --git a/daemon/src/db.rs b/daemon/src/db.rs index 1b0159a..1ce6e8e 100644 --- a/daemon/src/db.rs +++ b/daemon/src/db.rs @@ -241,6 +241,82 @@ async fn load_latest_cfd_state( Ok(latest_cfd_state_in_db) } +pub async fn load_cfd_by_order_id( + order_id: OrderId, + conn: &mut PoolConnection, +) -> anyhow::Result { + let order_uuid = serde_json::to_string(&order_id)?; + + let row = sqlx::query!( + r#" + select + cfds.id as cfd_id, + orders.uuid as order_id, + orders.initial_price as price, + orders.min_quantity as min_quantity, + orders.max_quantity as max_quantity, + orders.leverage as leverage, + orders.trading_pair as trading_pair, + orders.position as position, + orders.origin as origin, + orders.liquidation_price as liquidation_price, + orders.creation_timestamp as creation_timestamp, + orders.term as term, + cfds.quantity_usd as quantity_usd, + cfd_states.state as state + from cfds as cfds + inner join orders as orders on cfds.order_uuid = ? + inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id + where cfd_states.state in ( + select + state + from cfd_states + where cfd_id = cfds.id + order by id desc + limit 1 + ) + "#, + order_uuid + ) + .fetch_one(conn) + .await?; + + let order_id = serde_json::from_str(row.order_id.as_str()).unwrap(); + let trading_pair = serde_json::from_str(row.trading_pair.as_str()).unwrap(); + let position: Position = serde_json::from_str(row.position.as_str()).unwrap(); + let price = serde_json::from_str(row.price.as_str()).unwrap(); + let min_quantity = serde_json::from_str(row.min_quantity.as_str()).unwrap(); + let max_quantity = serde_json::from_str(row.max_quantity.as_str()).unwrap(); + let leverage = Leverage(row.leverage.try_into().unwrap()); + let liquidation_price = serde_json::from_str(row.liquidation_price.as_str()).unwrap(); + let creation_timestamp = serde_json::from_str(row.creation_timestamp.as_str()).unwrap(); + let term = serde_json::from_str(row.term.as_str()).unwrap(); + let origin: Origin = serde_json::from_str(row.origin.as_str()).unwrap(); + + let quantity = serde_json::from_str(row.quantity_usd.as_str()).unwrap(); + let latest_state = serde_json::from_str(row.state.as_str()).unwrap(); + + let order = Order { + id: order_id, + trading_pair, + position, + price, + min_quantity, + max_quantity, + leverage, + liquidation_price, + creation_timestamp, + term, + origin, + }; + + Ok(Cfd { + order, + quantity_usd: quantity, + state: latest_state, + }) +} + /// Loads all CFDs with the latest state as the CFD state pub async fn load_all_cfds(conn: &mut PoolConnection) -> anyhow::Result> { // TODO: Could be optimized with something like but not sure it's worth the complexity: @@ -374,6 +450,32 @@ mod tests { assert_eq!(cfd, cfd_from_db) } + #[tokio::test] + async fn test_insert_and_load_cfd_by_order_id() { + let pool = setup_test_db().await; + let mut conn = pool.acquire().await.unwrap(); + + let order = Order::from_default_with_price(Usd(dec!(10000)), Origin::Theirs).unwrap(); + let cfd = Cfd::new( + order.clone(), + Usd(dec!(1000)), + CfdState::PendingTakeRequest { + common: CfdStateCommon { + transition_timestamp: SystemTime::now(), + }, + }, + ); + + let order_id = order.id; + + // the order ahs to exist in the db in order to be able to insert the cfd + insert_order(&order, &mut conn).await.unwrap(); + insert_cfd(cfd.clone(), &mut conn).await.unwrap(); + + let cfd_from_db = load_cfd_by_order_id(order_id, &mut conn).await.unwrap(); + assert_eq!(cfd, cfd_from_db) + } + #[tokio::test] async fn test_insert_new_cfd_state() { let pool = setup_test_db().await; diff --git a/daemon/src/maker_cfd_actor.rs b/daemon/src/maker_cfd_actor.rs index 9bb4c6d..1e37d66 100644 --- a/daemon/src/maker_cfd_actor.rs +++ b/daemon/src/maker_cfd_actor.rs @@ -1,14 +1,12 @@ -use std::time::SystemTime; - -use crate::db::{insert_cfd, insert_order, load_all_cfds, load_order_by_id}; +use crate::db::{insert_cfd, insert_order, load_all_cfds, load_cfd_by_order_id, load_order_by_id}; use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; use crate::model::{TakerId, Usd}; use crate::wallet::Wallet; use crate::wire::SetupMsg; use crate::{maker_cfd_actor, maker_inc_connections_actor, setup_contract_actor}; use bdk::bitcoin::secp256k1::schnorrsig; -use bdk::bitcoin::{self}; use futures::Future; +use std::time::SystemTime; use tokio::sync::{mpsc, watch}; #[allow(clippy::large_enum_variant)] @@ -156,12 +154,10 @@ pub fn new( // Kick-off the CFD protocol let (sk, pk) = crate::keypair::new(&mut rand::thread_rng()); - // TODO: Load correct quantity from DB with order_id - let maker_params = wallet - .build_party_params(bitcoin::Amount::ZERO, pk) - .unwrap(); + let cfd = load_cfd_by_order_id(order_id, &mut conn).await.unwrap(); + let margin = cfd.calc_margin().unwrap(); - let cfd = load_order_by_id(order_id, &mut conn).await.unwrap(); + let maker_params = wallet.build_party_params(margin, pk).unwrap(); let (actor, inbox) = setup_contract_actor::new( { diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index de09799..fd5617c 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -277,6 +277,17 @@ impl Cfd { Ok(margin) } + pub fn calc_counterparty_margin(&self) -> Result { + let margin = match self.position() { + Position::Buy => calculate_sell_margin(self.order.price, self.quantity_usd)?, + Position::Sell => { + calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage)? + } + }; + + Ok(margin) + } + pub fn calc_profit(&self, current_price: Usd) -> Result<(Amount, Usd)> { let profit = calculate_profit(self.order.price, current_price, dec!(0.005), Usd(dec!(0.1)))?; diff --git a/daemon/src/setup_contract_actor.rs b/daemon/src/setup_contract_actor.rs index c1dd6bf..99fa0ca 100644 --- a/daemon/src/setup_contract_actor.rs +++ b/daemon/src/setup_contract_actor.rs @@ -1,4 +1,4 @@ -use crate::model::cfd::{AsBlocks, FinalizedCfd, Order}; +use crate::model::cfd::{AsBlocks, Cfd, FinalizedCfd}; use crate::wire::{AdaptorSignature, Msg0, Msg1, SetupMsg}; use anyhow::{Context, Result}; use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey, Signature, SECP256K1}; @@ -37,7 +37,7 @@ pub fn new( own_params: OwnParams, sk: SecretKey, oracle_pk: schnorrsig::PublicKey, - order: Order, + cfd: Cfd, ) -> ( impl Future, mpsc::UnboundedSender, @@ -66,11 +66,15 @@ pub fn new( AllParams::new(own, own_punish, other, other_punish, own_role) }; + if params.other.lock_amount != cfd.calc_counterparty_margin().unwrap() { + panic!("Sorry, have to panic 😬 - the amounts that the counterparty sent were wrong, expected {} actual {}", cfd.calc_counterparty_margin().unwrap(), params.other.lock_amount) + } + let own_cfd_txs = create_cfd_transactions( (params.maker().clone(), *params.maker_punish()), (params.taker().clone(), *params.taker_punish()), oracle_pk, - order.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32, + cfd.order.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32, vec![], sk, ) diff --git a/daemon/src/taker_cfd_actor.rs b/daemon/src/taker_cfd_actor.rs index d8b18fb..1f6113e 100644 --- a/daemon/src/taker_cfd_actor.rs +++ b/daemon/src/taker_cfd_actor.rs @@ -1,5 +1,6 @@ use crate::db::{ - insert_cfd, insert_new_cfd_state_by_order_id, insert_order, load_all_cfds, load_order_by_id, + insert_cfd, insert_new_cfd_state_by_order_id, insert_order, load_all_cfds, + load_cfd_by_order_id, load_order_by_id, }; use crate::model::cfd::{Cfd, CfdState, CfdStateCommon, FinalizedCfd, Order, OrderId}; use crate::model::Usd; @@ -7,7 +8,6 @@ use crate::wallet::Wallet; use crate::wire::SetupMsg; use crate::{setup_contract_actor, wire}; use bdk::bitcoin::secp256k1::schnorrsig; -use bdk::bitcoin::{self}; use core::panic; use futures::Future; use std::time::SystemTime; @@ -101,11 +101,10 @@ pub fn new( let (sk, pk) = crate::keypair::new(&mut rand::thread_rng()); - let taker_params = wallet - .build_party_params(bitcoin::Amount::ZERO, pk) // TODO: Load correct quantity from DB - .unwrap(); + let cfd = load_cfd_by_order_id(order_id, &mut conn).await.unwrap(); + let margin = cfd.calc_margin().unwrap(); - let cfd = load_order_by_id(order_id, &mut conn).await.unwrap(); + let taker_params = wallet.build_party_params(margin, pk).unwrap(); let (actor, inbox) = setup_contract_actor::new( { @@ -139,6 +138,8 @@ pub fn new( } Command::CfdSetupCompleted(_finalized_cfd) => { todo!("but what?") + + // Assumption: The maker publishes the CFD on chain } } }