diff --git a/bouncer/tests/btc_vault_swap.ts b/bouncer/tests/btc_vault_swap.ts index a80cf72edb..6c0550e6e0 100644 --- a/bouncer/tests/btc_vault_swap.ts +++ b/bouncer/tests/btc_vault_swap.ts @@ -24,6 +24,7 @@ interface VaultSwapDetails { chain: string; nulldata_payload: string; deposit_address: string; + expires_at: number; } interface Beneficiary { @@ -66,6 +67,20 @@ async function buildAndSendBtcVaultSwap( assert.strictEqual(vaultSwapDetails.chain, 'Bitcoin'); testBtcVaultSwap.debugLog('nulldata_payload:', vaultSwapDetails.nulldata_payload); testBtcVaultSwap.debugLog('deposit_address:', vaultSwapDetails.deposit_address); + testBtcVaultSwap.debugLog('expires_at:', vaultSwapDetails.expires_at); + + // Calculate expected expiry time assuming block time is 6 secs, expires_at = time left to next rotation + const epochDuration = (await chainflip.rpc(`cf_epoch_duration`)) as number; + const epochStartedAt = (await chainflip.rpc(`cf_current_epoch_started_at`)) as number; + const currentBlockNumber = (await chainflip.rpc.chain.getHeader()).number.toNumber(); + const blocksUntilNextRotation = epochDuration + epochStartedAt - currentBlockNumber; + const expectedExpiresAt = Date.now() + blocksUntilNextRotation * 6000; + // Check that expires_at field is correct (within 20 secs drift) + assert( + Math.abs(expectedExpiresAt - vaultSwapDetails.expires_at) <= 20 * 1000, + `VaultSwapDetails expiry timestamp is not within a 20 secs drift of the expected expiry time. + expectedExpiresAt = ${expectedExpiresAt} and actualExpiresAt = ${vaultSwapDetails.expires_at}`, + ); const txid = await sendVaultTransaction( vaultSwapDetails.nulldata_payload, diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 958fe639d2..d93fee2869 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -124,7 +124,10 @@ use sp_runtime::{ BoundedVec, }; -use frame_support::genesis_builder_helper::build_state; +use frame_support::{ + genesis_builder_helper::build_state, + traits::{EstimateNextSessionRotation, Time}, +}; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; use sp_runtime::{ @@ -2241,6 +2244,23 @@ impl_runtime_apis! { }) .map_err(|_| pallet_cf_swapping::Error::::InvalidDestinationAddress)?; + // Payload expiry time is set to time left to next rotation. + // * For BTC: the actual expiry time is 2 rotations, but we deliberately set expires_at to be the time left to next + // rotation to cater for the case when a forced rotation happens between when the payload was requested and + // before expires_at. BTC funds can be lost in 3 cases: + // * User makes the vault transaction after expires_at, and it happens that we did a forced rotation in between + // * User submits the vault transaction 3 days (1 epoch) after expires_at, and with no forced rotations in between + // * We do 2 forced rotations in between payload request and expires_at + // * For SOLANA: The actual expiry time is indeed time left to next rotation. If a forced rotation + // happens in between as explained before, it is actually not a problem as the user won't lose any funds. + // * For ETH: payload never expires hence we don't send expires_at + let current_block = System::block_number(); + let (Some(next_rotation_block), _) = Validator::estimate_next_session_rotation(current_block) else { + Err(pallet_cf_validator::Error::::InvalidEpochDuration)? + }; + let blocks_until_next_rotation = next_rotation_block.saturating_sub(current_block); + let expires_at = Timestamp::now() + blocks_until_next_rotation as u64 * SLOT_DURATION; + // Encode swap match ForeignChain::from(source_asset) { ForeignChain::Bitcoin => { @@ -2286,9 +2306,12 @@ impl_runtime_apis! { }, }; + + Ok(VaultSwapDetails::Bitcoin { nulldata_payload: encode_swap_params_in_nulldata_payload(params), deposit_address: derive_btc_vault_deposit_address(private_channel_id), + expires_at }) }, _ => Err(pallet_cf_swapping::Error::::UnsupportedSourceAsset.into()), diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index beff01bcd7..536b7dec2a 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -43,6 +43,8 @@ pub enum VaultSwapDetails { #[serde(with = "sp_core::bytes")] nulldata_payload: Vec, deposit_address: BtcAddress, + /// Payload expiry time, expressed as timestamp since the UNIX_EPOCH in milliseconds + expires_at: u64, }, } @@ -52,8 +54,12 @@ impl VaultSwapDetails { F: FnOnce(BtcAddress) -> T, { match self { - VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address } => - VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address: f(deposit_address) }, + VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address, expires_at } => + VaultSwapDetails::Bitcoin { + nulldata_payload, + deposit_address: f(deposit_address), + expires_at, + }, } } }