Skip to content
This repository was archived by the owner on Sep 12, 2023. It is now read-only.

Commit 601989d

Browse files
bors[bot]luckysori
andauthored
Merge #1618
1618: Calculate `hours_to_charge` for rollover correctly r=luckysori a=luckysori Fixes #1125. Previously we always assumed that rollover would consistently happen every hour. Obviously parties can go offline, so hard-coding a value of 1 hour was naive. By looking at the timestamp of the the `settlement_event_id` (the time at which the oracle will non-collaboratively settle the CFD) we can figure out how long is left of the time-to-live of the CFD and therefore by how many hours we need to extend it to restore a time-to-live equal to the `SETTLEMENT_INTERVAL`. We can use the latter number to determine how many hours we should charge for the rollover. Co-authored-by: Lucas Soriano del Pino <[email protected]>
2 parents 504b4af + 993e20c commit 601989d

File tree

1 file changed

+115
-8
lines changed

1 file changed

+115
-8
lines changed

model/src/cfd.rs

+115-8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ use maia::secp256k1_zkp::SECP256K1;
4848
use maia::spending_tx_sighash;
4949
use maia::Payout;
5050
use maia::TransactionExt;
51+
use num::Zero;
5152
use rust_decimal::Decimal;
5253
use rust_decimal_macros::dec;
5354
use serde::de::Error as _;
@@ -841,15 +842,14 @@ impl Cfd {
841842
bail!("Can only accept proposal as a maker");
842843
}
843844

844-
let hours_to_charge = 1;
845-
845+
let hours_to_charge = self.hours_to_extend_in_rollover()?;
846846
let funding_fee = calculate_funding_fee(
847847
self.initial_price,
848848
self.quantity,
849849
self.long_leverage,
850850
self.short_leverage,
851851
funding_rate,
852-
hours_to_charge,
852+
hours_to_charge as i64,
853853
)?;
854854

855855
Ok((
@@ -885,16 +885,14 @@ impl Cfd {
885885

886886
self.can_rollover()?;
887887

888-
// TODO: Taker should take this from the maker, optionally calculate and verify
889-
// whether they match
890-
let hours_to_charge = 1;
888+
let hours_to_charge = self.hours_to_extend_in_rollover()?;
891889
let funding_fee = calculate_funding_fee(
892890
self.initial_price,
893891
self.quantity,
894892
self.long_leverage,
895893
self.short_leverage,
896894
funding_rate,
897-
hours_to_charge,
895+
hours_to_charge as i64,
898896
)?;
899897

900898
Ok((
@@ -1336,6 +1334,43 @@ impl Cfd {
13361334
Ok((tx, sig, script_pk))
13371335
}
13381336

1337+
/// Number of hours that the time-to-live of the contract will be
1338+
/// extended by with the next rollover.
1339+
///
1340+
/// During rollover the time-to-live of the contract is extended
1341+
/// so that the non-collaborative settlement time is set to ~24
1342+
/// hours in the future from now.
1343+
fn hours_to_extend_in_rollover(&self) -> Result<u64> {
1344+
let dlc = self.dlc.as_ref().context("Cannot roll over without DLC")?;
1345+
let settlement_time = dlc.settlement_event_id.timestamp();
1346+
1347+
let hours_left = settlement_time - OffsetDateTime::now_utc();
1348+
1349+
if !hours_left.is_positive() {
1350+
tracing::warn!("Rolling over a contract that can be settled non-collaboratively");
1351+
1352+
return Ok(SETTLEMENT_INTERVAL.whole_hours() as u64);
1353+
}
1354+
1355+
let time_to_extend = SETTLEMENT_INTERVAL
1356+
.checked_sub(hours_left)
1357+
.context("Subtraction overflow")?;
1358+
let hours_to_extend = time_to_extend.whole_hours();
1359+
1360+
if hours_to_extend.is_negative() {
1361+
bail!(
1362+
"Cannot rollover if time-to-live of contract is > {} hours",
1363+
SETTLEMENT_INTERVAL.whole_hours()
1364+
);
1365+
}
1366+
1367+
Ok(if hours_to_extend.is_zero() {
1368+
1
1369+
} else {
1370+
hours_to_extend as u64
1371+
})
1372+
}
1373+
13391374
pub fn version(&self) -> u64 {
13401375
self.version
13411376
}
@@ -1935,9 +1970,11 @@ mod tests {
19351970
use bdk_ext::AddressExt;
19361971
use bdk_ext::SecretKeyExt;
19371972
use maia::lock_descriptor;
1973+
use proptest::prelude::*;
19381974
use rust_decimal_macros::dec;
19391975
use std::collections::BTreeMap;
19401976
use std::str::FromStr;
1977+
use time::ext::NumericalDuration;
19411978
use time::macros::datetime;
19421979

19431980
#[test]
@@ -2921,6 +2958,77 @@ mod tests {
29212958
assert_eq!(is_liquidation_price, should_liquidation_price);
29222959
}
29232960

2961+
#[test]
2962+
fn correctly_calculate_hours_to_extend_in_rollover() {
2963+
let settlement_interval = SETTLEMENT_INTERVAL.whole_hours();
2964+
for hour in 0..settlement_interval {
2965+
let event_id_in_x_hours =
2966+
BitMexPriceEventId::with_20_digits(OffsetDateTime::now_utc() + hour.hours());
2967+
2968+
let taker = Cfd::dummy_taker_long().dummy_open(event_id_in_x_hours);
2969+
let maker = Cfd::dummy_maker_short().dummy_open(event_id_in_x_hours);
2970+
2971+
assert_eq!(
2972+
taker.hours_to_extend_in_rollover().unwrap(),
2973+
(settlement_interval - hour) as u64
2974+
);
2975+
2976+
assert_eq!(
2977+
maker.hours_to_extend_in_rollover().unwrap(),
2978+
(settlement_interval - hour) as u64
2979+
);
2980+
}
2981+
}
2982+
2983+
#[test]
2984+
fn rollover_extends_time_to_live_by_settlement_interval_if_cfd_can_be_settled() {
2985+
let event_id_1_hour_ago =
2986+
BitMexPriceEventId::with_20_digits(OffsetDateTime::now_utc() - 1.hours());
2987+
2988+
let taker = Cfd::dummy_taker_long().dummy_open(event_id_1_hour_ago);
2989+
let maker = Cfd::dummy_maker_short().dummy_open(event_id_1_hour_ago);
2990+
2991+
assert_eq!(
2992+
taker.hours_to_extend_in_rollover().unwrap(),
2993+
SETTLEMENT_INTERVAL.whole_hours() as u64
2994+
);
2995+
2996+
assert_eq!(
2997+
maker.hours_to_extend_in_rollover().unwrap(),
2998+
SETTLEMENT_INTERVAL.whole_hours() as u64
2999+
);
3000+
}
3001+
3002+
#[test]
3003+
fn cannot_rollover_if_time_to_live_is_longer_than_settlement_interval() {
3004+
let more_than_settlement_interval_hours = SETTLEMENT_INTERVAL.whole_hours() + 2;
3005+
let event_id_way_in_the_future = BitMexPriceEventId::with_20_digits(
3006+
OffsetDateTime::now_utc() + more_than_settlement_interval_hours.hours(),
3007+
);
3008+
3009+
let taker = Cfd::dummy_taker_long().dummy_open(event_id_way_in_the_future);
3010+
let maker = Cfd::dummy_maker_short().dummy_open(event_id_way_in_the_future);
3011+
3012+
taker.hours_to_extend_in_rollover().unwrap_err();
3013+
maker.hours_to_extend_in_rollover().unwrap_err();
3014+
}
3015+
3016+
proptest! {
3017+
#[test]
3018+
fn rollover_extended_by_one_hour_if_time_to_live_is_within_one_hour_of_settlement_interval(minutes in 0i64..=60) {
3019+
let close_to_settlement_interval = SETTLEMENT_INTERVAL + minutes.minutes();
3020+
let event_id_within_the_hour = BitMexPriceEventId::with_20_digits(
3021+
OffsetDateTime::now_utc() + close_to_settlement_interval,
3022+
);
3023+
3024+
let taker = Cfd::dummy_taker_long().dummy_open(event_id_within_the_hour);
3025+
let maker = Cfd::dummy_maker_short().dummy_open(event_id_within_the_hour);
3026+
3027+
prop_assert_eq!(taker.hours_to_extend_in_rollover().unwrap(), 1);
3028+
prop_assert_eq!(maker.hours_to_extend_in_rollover().unwrap(), 1);
3029+
}
3030+
}
3031+
29243032
#[allow(clippy::too_many_arguments)]
29253033
fn collab_settlement_taker_long_maker_short(
29263034
quantity: Usd,
@@ -2970,7 +3078,6 @@ mod tests {
29703078
(taker_payout.as_sat(), maker_payout.as_sat())
29713079
}
29723080

2973-
use proptest::prelude::*;
29743081
use rand::thread_rng;
29753082
proptest! {
29763083
#[test]

0 commit comments

Comments
 (0)