@@ -48,6 +48,7 @@ use maia::secp256k1_zkp::SECP256K1;
48
48
use maia:: spending_tx_sighash;
49
49
use maia:: Payout ;
50
50
use maia:: TransactionExt ;
51
+ use num:: Zero ;
51
52
use rust_decimal:: Decimal ;
52
53
use rust_decimal_macros:: dec;
53
54
use serde:: de:: Error as _;
@@ -841,15 +842,14 @@ impl Cfd {
841
842
bail ! ( "Can only accept proposal as a maker" ) ;
842
843
}
843
844
844
- let hours_to_charge = 1 ;
845
-
845
+ let hours_to_charge = self . hours_to_extend_in_rollover ( ) ?;
846
846
let funding_fee = calculate_funding_fee (
847
847
self . initial_price ,
848
848
self . quantity ,
849
849
self . long_leverage ,
850
850
self . short_leverage ,
851
851
funding_rate,
852
- hours_to_charge,
852
+ hours_to_charge as i64 ,
853
853
) ?;
854
854
855
855
Ok ( (
@@ -885,16 +885,14 @@ impl Cfd {
885
885
886
886
self . can_rollover ( ) ?;
887
887
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 ( ) ?;
891
889
let funding_fee = calculate_funding_fee (
892
890
self . initial_price ,
893
891
self . quantity ,
894
892
self . long_leverage ,
895
893
self . short_leverage ,
896
894
funding_rate,
897
- hours_to_charge,
895
+ hours_to_charge as i64 ,
898
896
) ?;
899
897
900
898
Ok ( (
@@ -1336,6 +1334,43 @@ impl Cfd {
1336
1334
Ok ( ( tx, sig, script_pk) )
1337
1335
}
1338
1336
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
+
1339
1374
pub fn version ( & self ) -> u64 {
1340
1375
self . version
1341
1376
}
@@ -1935,9 +1970,11 @@ mod tests {
1935
1970
use bdk_ext:: AddressExt ;
1936
1971
use bdk_ext:: SecretKeyExt ;
1937
1972
use maia:: lock_descriptor;
1973
+ use proptest:: prelude:: * ;
1938
1974
use rust_decimal_macros:: dec;
1939
1975
use std:: collections:: BTreeMap ;
1940
1976
use std:: str:: FromStr ;
1977
+ use time:: ext:: NumericalDuration ;
1941
1978
use time:: macros:: datetime;
1942
1979
1943
1980
#[ test]
@@ -2921,6 +2958,77 @@ mod tests {
2921
2958
assert_eq ! ( is_liquidation_price, should_liquidation_price) ;
2922
2959
}
2923
2960
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
+
2924
3032
#[ allow( clippy:: too_many_arguments) ]
2925
3033
fn collab_settlement_taker_long_maker_short (
2926
3034
quantity : Usd ,
@@ -2970,7 +3078,6 @@ mod tests {
2970
3078
( taker_payout. as_sat ( ) , maker_payout. as_sat ( ) )
2971
3079
}
2972
3080
2973
- use proptest:: prelude:: * ;
2974
3081
use rand:: thread_rng;
2975
3082
proptest ! {
2976
3083
#[ test]
0 commit comments