diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 848f214..7522bc0 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -299,6 +299,20 @@ fn calculate_profit( Ok((Amount::ZERO, Usd::ZERO)) } +pub trait AsBlocks { + /// Calculates the duration in Bitcoin blocks. + /// + /// On Bitcoin there is a block every 10 minutes/600 seconds on average. + /// It's the caller's responsibility to round the resulting floating point number. + fn as_blocks(&self) -> f32; +} + +impl AsBlocks for Duration { + fn as_blocks(&self) -> f32 { + self.as_secs_f32() / 60.0 / 10.0 + } +} + /// Calculates the buyer's margin in BTC /// /// The margin is the initial margin and represents the collateral the buyer has to come up with to @@ -510,6 +524,23 @@ mod tests { r#"{"type":"Error","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}}"# ); } + + #[test] + fn test_secs_into_blocks() { + let error_margin = f32::EPSILON; + + let duration = Duration::from_secs(600); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 1.0 && blocks + error_margin > 1.0); + + let duration = Duration::from_secs(0); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 0.0 && blocks + error_margin > 0.0); + + let duration = Duration::from_secs(60); + let blocks = duration.as_blocks(); + assert!(blocks - error_margin < 0.1 && blocks + error_margin > 0.1); + } } /// Contains all data we've assembled about the CFD through the setup protocol. diff --git a/daemon/src/taker_cfd_actor.rs b/daemon/src/taker_cfd_actor.rs index e3176c8..6e4a744 100644 --- a/daemon/src/taker_cfd_actor.rs +++ b/daemon/src/taker_cfd_actor.rs @@ -2,7 +2,9 @@ use crate::db::{ insert_cfd, insert_cfd_offer, insert_new_cfd_state_by_offer_id, load_all_cfds, load_offer_by_id, OfferOrigin, }; -use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd}; +use crate::model::cfd::{ + AsBlocks, Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd, +}; use crate::model::Usd; use crate::wire; use crate::wire::{AdaptorSignature, Msg0, Msg1, SetupMsg}; @@ -21,6 +23,20 @@ use std::collections::HashMap; use std::time::SystemTime; use tokio::sync::{mpsc, watch}; +/// A factor to be added to the CFD offer term for calculating the refund timelock. +/// +/// The refund timelock is important in case the oracle disappears or never publishes a signature. +/// Ideally, both users collaboratively settle in the refund scenario. This factor is important if +/// the users do not settle collaboratively. +/// `1.5` times the term as defined in CFD offer should be safe in the extreme case where a user +/// publishes the commit transaction right after the contract was initialized. In this case, the +/// oracle still has `1.0 * cfdoffer.term` time to attest and no one can publish the refund +/// transaction. +/// The downside is that if the oracle disappears: the users would only notice at the end +/// of the cfd term. In this case the users has to wait for another `1.5` times of the +/// term to get his funds back. +pub const REFUND_THRESHOLD: f32 = 1.5; + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum Command { @@ -119,15 +135,17 @@ where .build_party_params(bitcoin::Amount::ZERO, pk) // TODO: Load correct quantity from DB .unwrap(); + let cfd = load_offer_by_id(offer_id, &mut conn).await.unwrap(); + let (actor, inbox) = setup_contract( { let inbox = out_msg_maker_inbox.clone(); - move |msg| inbox.send(wire::TakerToMaker::Protocol(msg)).unwrap() }, taker_params, sk, oracle_pk, + cfd, ); tokio::spawn({ @@ -170,6 +188,7 @@ fn setup_contract( taker: PartyParams, sk: SecretKey, oracle_pk: schnorrsig::PublicKey, + offer: CfdOffer, ) -> ( impl Future, mpsc::UnboundedSender, @@ -193,7 +212,7 @@ fn setup_contract( (maker.clone(), maker_punish), (taker.clone(), taker_punish), oracle_pk, - 0, // TODO: Calculate refund timelock based on CFD term + offer.term.mul_f32(REFUND_THRESHOLD).as_blocks().ceil() as u32, vec![], sk, )