diff --git a/Cargo.lock b/Cargo.lock index e1a72fa..34ad884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3715,7 +3715,7 @@ dependencies = [ [[package]] name = "satoshi-bridge" -version = "0.7.0" +version = "0.8.0" dependencies = [ "bitcoin", "core2", diff --git a/contracts/satoshi-bridge/Cargo.toml b/contracts/satoshi-bridge/Cargo.toml index 93a11c8..8dbd7c8 100644 --- a/contracts/satoshi-bridge/Cargo.toml +++ b/contracts/satoshi-bridge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "satoshi-bridge" -version = "0.7.0" +version = "0.8.0" edition.workspace = true publish.workspace = true repository.workspace = true diff --git a/contracts/satoshi-bridge/src/api/bridge.rs b/contracts/satoshi-bridge/src/api/bridge.rs index 2a69d6e..6e6dfeb 100644 --- a/contracts/satoshi-bridge/src/api/bridge.rs +++ b/contracts/satoshi-bridge/src/api/bridge.rs @@ -449,6 +449,7 @@ impl Contract { max_gas_fee: gas_fee, last_rbf_time_sec: None, cancel_rbf_reserved: None, + subsidize_amount: 0, }), }; require!( diff --git a/contracts/satoshi-bridge/src/api/token_receiver.rs b/contracts/satoshi-bridge/src/api/token_receiver.rs index cfaa05e..538ad7a 100644 --- a/contracts/satoshi-bridge/src/api/token_receiver.rs +++ b/contracts/satoshi-bridge/src/api/token_receiver.rs @@ -14,6 +14,10 @@ pub enum TokenReceiverMessage { output: Vec, max_gas_fee: Option, }, + Rbf { + pending_tx_id: String, + output: Vec, + }, } #[near] @@ -26,10 +30,6 @@ impl FungibleTokenReceiver for Contract { msg: String, ) -> PromiseOrValue { let amount = amount.into(); - require!( - amount >= self.internal_config().min_withdraw_amount, - "Invalid amount" - ); let message = serde_json::from_str::(&msg).expect("INVALID MSG"); let token_id = env::predecessor_account_id(); require!( @@ -52,14 +52,24 @@ impl FungibleTokenReceiver for Contract { input, output, max_gas_fee, - } => self.ft_on_transfer_withdraw_chain_specific( - sender_id, - amount, - target_btc_address, - input, + } => { + require!( + amount >= self.internal_config().min_withdraw_amount, + "Invalid amount" + ); + self.ft_on_transfer_withdraw_chain_specific( + sender_id, + amount, + target_btc_address, + input, + output, + max_gas_fee, + ) + } + TokenReceiverMessage::Rbf { + pending_tx_id, output, - max_gas_fee, - ), + } => self.rbf_subsidize_chain_specific(amount, sender_id, pending_tx_id, output), } } } @@ -119,6 +129,7 @@ impl Contract { max_gas_fee: gas_fee, last_rbf_time_sec: None, cancel_rbf_reserved: None, + subsidize_amount: 0, }), }; require!( diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs index e3bb76d..e70a984 100644 --- a/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs +++ b/contracts/satoshi-bridge/src/bitcoin_utils/contract_methods.rs @@ -11,7 +11,7 @@ macro_rules! define_rbf_method { account_id: AccountId, original_btc_pending_verify_id: String, output: Vec, - ) { + ) -> String { let original_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id); @@ -31,6 +31,8 @@ macro_rules! define_rbf_method { btc_pending_id: &btc_pending_id, } .emit(); + + btc_pending_id } }; } @@ -45,7 +47,10 @@ impl Contract { // Ensure that the RBF transaction pays more gas than the previous transaction. let max_gas_fee = original_tx_btc_pending_info.get_max_gas_fee(); let additional_gas_amount = gas_fee.saturating_sub(max_gas_fee); - require!(additional_gas_amount > 0, "No gas increase."); + require!( + additional_gas_amount > 0, + format!("No gas increase. Old gas fee = {max_gas_fee}, new gas fee = {gas_fee}") + ); } pub(crate) fn ft_on_transfer_withdraw_chain_specific( @@ -97,4 +102,60 @@ impl Contract { let original_psbt = original_tx_btc_pending_info.get_psbt(); PsbtWrapper::from_original_psbt(original_psbt, output) } + + pub(crate) fn rbf_subsidize_chain_specific( + &mut self, + amount: u128, + sender_id: AccountId, + pending_tx_id: String, + output: Vec, + ) -> PromiseOrValue { + let origin_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&pending_tx_id); + let user_account_id = origin_tx_btc_pending_info.account_id.clone(); + require!( + self.internal_unwrap_account(&user_account_id) + .btc_pending_sign_id + .is_none(), + "Assisted user previous btc tx has not been signed" + ); + let full_subsidy_amount = self + .internal_unwrap_btc_pending_info(&pending_tx_id) + .get_subsidize_amount() + + amount; + self.internal_unwrap_mut_btc_pending_info(&pending_tx_id) + .update_subsidize_amount(full_subsidy_amount); + + let new_pending_info_id = self.withdraw_rbf_chain_specific( + user_account_id.clone(), + pending_tx_id.clone(), + output, + ); + + let origin_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&pending_tx_id); + let new_tx_btc_pending_info = self.internal_unwrap_btc_pending_info(&new_pending_info_id); + + require!( + new_tx_btc_pending_info.actual_received_amount + == origin_tx_btc_pending_info.actual_received_amount, + "Actual received amount has been changed." + ); + let gas_fee_diff = new_tx_btc_pending_info + .gas_fee + .saturating_sub(origin_tx_btc_pending_info.gas_fee); + require!( + gas_fee_diff == full_subsidy_amount, + "Gas fee diff is not equal to subsidy amount." + ); + + Event::SubsidizeRbf { + origin_btc_pending_id: &pending_tx_id, + subsidy_amount: U128(amount), + full_subsidy_amount: U128(full_subsidy_amount), + subsidizer: &sender_id, + beneficiary: &user_account_id, + } + .emit(); + + PromiseOrValue::Value(U128(0)) + } } diff --git a/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs b/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs index cea9346..436c004 100644 --- a/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs +++ b/contracts/satoshi-bridge/src/bitcoin_utils/psbt_wrapper.rs @@ -42,7 +42,7 @@ impl PsbtWrapper { original_psbt: crate::psbt_wrapper::PsbtWrapper, output: Vec, ) -> Self { - let sequence = bitcoin::Sequence::MAX; + let sequence = bitcoin::Sequence::ENABLE_RBF_NO_LOCKTIME; let transaction = BtcTransaction { version: Version::TWO, diff --git a/contracts/satoshi-bridge/src/btc_pending_info.rs b/contracts/satoshi-bridge/src/btc_pending_info.rs index c519c03..0adfe41 100644 --- a/contracts/satoshi-bridge/src/btc_pending_info.rs +++ b/contracts/satoshi-bridge/src/btc_pending_info.rs @@ -12,6 +12,8 @@ pub struct OriginalState { pub max_gas_fee: u128, pub last_rbf_time_sec: Option, pub cancel_rbf_reserved: Option, + #[serde(with = "u128_dec_format")] + pub subsidize_amount: u128, } impl OriginalState { @@ -199,6 +201,28 @@ impl BTCPendingInfo { _ => env::panic_str("Not original tx"), } } + + pub fn get_subsidize_amount(&self) -> u128 { + match self.state.borrow() { + PendingInfoState::WithdrawOriginal(state) => state.subsidize_amount, + PendingInfoState::ActiveUtxoManagementOriginal(state) => state.subsidize_amount, + _ => env::panic_str("Not original tx"), + } + } + + pub fn update_subsidize_amount(&mut self, subsidize_amount: u128) { + match self.state.borrow_mut() { + PendingInfoState::WithdrawOriginal(state) => { + state.subsidize_amount = subsidize_amount; + state.last_rbf_time_sec = Some(nano_to_sec(env::block_timestamp())); + } + PendingInfoState::ActiveUtxoManagementOriginal(state) => { + state.subsidize_amount = subsidize_amount; + state.last_rbf_time_sec = Some(nano_to_sec(env::block_timestamp())); + } + _ => env::panic_str("Not original tx"), + } + } pub fn to_pending_verify_stage(&mut self) { match self.state.borrow_mut() { @@ -293,12 +317,14 @@ impl BTCPendingInfo { #[near(serializers = [borsh])] pub enum VBTCPendingInfo { + V0(BTCPendingInfoV0), Current(BTCPendingInfo), } impl From for BTCPendingInfo { fn from(v: VBTCPendingInfo) -> Self { match v { + VBTCPendingInfo::V0(c) => c.into(), VBTCPendingInfo::Current(c) => c, } } @@ -307,6 +333,7 @@ impl From for BTCPendingInfo { impl From<&VBTCPendingInfo> for BTCPendingInfo { fn from(v: &VBTCPendingInfo) -> Self { match v { + VBTCPendingInfo::V0(c) => c.clone().into(), VBTCPendingInfo::Current(c) => c.clone(), } } @@ -315,6 +342,7 @@ impl From<&VBTCPendingInfo> for BTCPendingInfo { impl<'a> From<&'a VBTCPendingInfo> for &'a BTCPendingInfo { fn from(v: &'a VBTCPendingInfo) -> Self { match v { + VBTCPendingInfo::V0(_) => unreachable!(), VBTCPendingInfo::Current(c) => c, } } @@ -323,6 +351,7 @@ impl<'a> From<&'a VBTCPendingInfo> for &'a BTCPendingInfo { impl<'a> From<&'a mut VBTCPendingInfo> for &'a mut BTCPendingInfo { fn from(v: &'a mut VBTCPendingInfo) -> Self { match v { + VBTCPendingInfo::V0(_) => unreachable!(), VBTCPendingInfo::Current(c) => c, } } @@ -361,11 +390,18 @@ impl Contract { &mut self, btc_pending_id: &String, ) -> &mut BTCPendingInfo { - self.data_mut() + let btc_pending_info = self + .data_mut() .btc_pending_infos .get_mut(btc_pending_id) - .map(|o| o.into()) - .expect("BTC pending info not exist") + .expect("BTC pending info not exist"); + + if let VBTCPendingInfo::V0(old) = &btc_pending_info { + let new_current = BTCPendingInfo::from(old.clone()); + *btc_pending_info = VBTCPendingInfo::Current(new_current); + } + + btc_pending_info.into() } pub fn internal_remove_btc_pending_info(&mut self, btc_pending_id: &String) -> BTCPendingInfo { diff --git a/contracts/satoshi-bridge/src/event.rs b/contracts/satoshi-bridge/src/event.rs index 6dbd4c7..37d343a 100644 --- a/contracts/satoshi-bridge/src/event.rs +++ b/contracts/satoshi-bridge/src/event.rs @@ -44,6 +44,13 @@ pub enum Event<'a> { account_id: &'a AccountId, btc_pending_id: &'a String, }, + SubsidizeRbf { + origin_btc_pending_id: &'a String, + subsidy_amount: U128, + full_subsidy_amount: U128, + subsidizer: &'a AccountId, + beneficiary: &'a AccountId, + }, BtcInputSignature { account_id: &'a AccountId, btc_pending_id: &'a String, diff --git a/contracts/satoshi-bridge/src/legacy.rs b/contracts/satoshi-bridge/src/legacy.rs index c766f19..1c5c3d8 100644 --- a/contracts/satoshi-bridge/src/legacy.rs +++ b/contracts/satoshi-bridge/src/legacy.rs @@ -467,3 +467,177 @@ impl From for ContractData { } } } + +#[near(serializers = [borsh, json])] +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub struct OriginalStateV0 { + pub stage: PendingInfoStage, + #[serde(with = "u128_dec_format")] + pub max_gas_fee: u128, + pub last_rbf_time_sec: Option, + pub cancel_rbf_reserved: Option, +} + +#[near(serializers = [borsh, json])] +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub enum PendingInfoStateV0 { + WithdrawOriginal(OriginalStateV0), + WithdrawUserRbf(RbfState), + WithdrawCancelRbf(RbfState), + ActiveUtxoManagementOriginal(OriginalStateV0), + ActiveUtxoManagementRbf(RbfState), + ActiveUtxoManagementCancelRbf(RbfState), +} + +#[near(serializers = [borsh, json])] +#[derive(Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))] +pub struct BTCPendingInfoV0 { + pub account_id: AccountId, + pub btc_pending_id: String, + #[serde(with = "u128_dec_format")] + pub transfer_amount: u128, + #[serde(with = "u128_dec_format")] + pub actual_received_amount: u128, + #[serde(with = "u128_dec_format")] + pub withdraw_fee: u128, + #[serde(with = "u128_dec_format")] + pub gas_fee: u128, + #[serde(with = "u128_dec_format")] + pub burn_amount: u128, + pub psbt_hex: String, + pub vutxos: Vec, + pub signatures: Vec>, + pub tx_bytes_with_sign: Option>, + pub create_time_sec: u32, + pub last_sign_time_sec: u32, + pub state: PendingInfoStateV0, +} + +impl From for OriginalState { + fn from(c: OriginalStateV0) -> Self { + Self { + stage: c.stage, + max_gas_fee: c.max_gas_fee, + last_rbf_time_sec: c.last_rbf_time_sec, + cancel_rbf_reserved: c.cancel_rbf_reserved, + subsidize_amount: 0, + } + } +} + +impl From for PendingInfoState { + fn from(c: PendingInfoStateV0) -> Self { + match c { + PendingInfoStateV0::WithdrawOriginal(x) => PendingInfoState::WithdrawOriginal(x.into()), + PendingInfoStateV0::WithdrawUserRbf(x) => PendingInfoState::WithdrawUserRbf(x), + PendingInfoStateV0::WithdrawCancelRbf(x) => PendingInfoState::WithdrawCancelRbf(x), + PendingInfoStateV0::ActiveUtxoManagementOriginal(x) => { + PendingInfoState::ActiveUtxoManagementOriginal(x.into()) + } + PendingInfoStateV0::ActiveUtxoManagementRbf(x) => { + PendingInfoState::ActiveUtxoManagementRbf(x) + } + PendingInfoStateV0::ActiveUtxoManagementCancelRbf(x) => { + PendingInfoState::ActiveUtxoManagementCancelRbf(x) + } + } + } +} + +impl From for BTCPendingInfo { + fn from(c: BTCPendingInfoV0) -> Self { + Self { + account_id: c.account_id, + btc_pending_id: c.btc_pending_id, + transfer_amount: c.transfer_amount, + actual_received_amount: c.actual_received_amount, + withdraw_fee: c.withdraw_fee, + gas_fee: c.gas_fee, + burn_amount: c.burn_amount, + psbt_hex: c.psbt_hex, + vutxos: c.vutxos, + signatures: c.signatures, + tx_bytes_with_sign: c.tx_bytes_with_sign, + create_time_sec: c.create_time_sec, + last_sign_time_sec: c.last_sign_time_sec, + state: c.state.into(), + } + } +} + +#[near(serializers = [borsh])] +pub struct ContractDataV3 { + pub config: LazyOption, + pub accounts: IterableMap, + pub utxos: IterableMap, + pub unavailable_utxos: IterableMap, + pub verified_deposit_utxo: LookupSet, + pub btc_pending_infos: IterableMap, + pub rbf_txs: IterableMap>, + pub relayer_white_list: IterableSet, + pub extra_msg_relayer_white_list: IterableSet, + pub post_action_receiver_id_white_list: IterableSet, + pub post_action_msg_templates: IterableMap>, + pub lost_found: IterableMap, + pub acc_collected_protocol_fee: u128, + pub cur_available_protocol_fee: u128, + pub acc_claimed_protocol_fee: u128, + pub cur_reserved_protocol_fee: u128, + pub acc_protocol_fee_for_gas: u128, +} + +impl From for ContractData { + fn from(c: ContractDataV3) -> Self { + let ContractDataV3 { + config, + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + mut btc_pending_infos, + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } = c; + + let keys: Vec = btc_pending_infos.keys().map(|k| k.clone()).collect(); + + for key in keys { + if let Some(value) = btc_pending_infos.get(&key) { + let current: BTCPendingInfo = value.into(); + btc_pending_infos.insert(key, VBTCPendingInfo::Current(current)); + } + } + + Self { + config, + accounts, + utxos, + unavailable_utxos, + verified_deposit_utxo, + btc_pending_infos, + rbf_txs, + relayer_white_list, + extra_msg_relayer_white_list, + post_action_receiver_id_white_list, + post_action_msg_templates, + lost_found, + acc_collected_protocol_fee, + cur_available_protocol_fee, + acc_claimed_protocol_fee, + cur_reserved_protocol_fee, + acc_protocol_fee_for_gas, + } + } +} diff --git a/contracts/satoshi-bridge/src/lib.rs b/contracts/satoshi-bridge/src/lib.rs index 149eb1b..98e70dd 100644 --- a/contracts/satoshi-bridge/src/lib.rs +++ b/contracts/satoshi-bridge/src/lib.rs @@ -125,6 +125,7 @@ pub enum VersionedContractData { V0(ContractDataV0), V1(ContractDataV1), V2(ContractDataV2), + V3(ContractDataV3), Current(ContractData), } diff --git a/contracts/satoshi-bridge/src/rbf/withdraw.rs b/contracts/satoshi-bridge/src/rbf/withdraw.rs index 90a1207..90097e6 100644 --- a/contracts/satoshi-bridge/src/rbf/withdraw.rs +++ b/contracts/satoshi-bridge/src/rbf/withdraw.rs @@ -6,6 +6,7 @@ impl Contract { &self, original_tx_btc_pending_info: &BTCPendingInfo, withdraw_rbf_psbt: &PsbtWrapper, + subsidy_amount: u128, ) -> (u128, u128) { let withdraw_change_address_script_pubkey = self.internal_config().get_change_script_pubkey(); @@ -27,7 +28,7 @@ impl Contract { &target_address_script_pubkey, &withdraw_change_address_script_pubkey, &original_tx_btc_pending_info.vutxos, - original_tx_btc_pending_info.transfer_amount, + original_tx_btc_pending_info.transfer_amount + subsidy_amount, original_tx_btc_pending_info.withdraw_fee, ); (actual_received_amount, gas_fee) @@ -55,13 +56,23 @@ impl Contract { original_tx_id: original_btc_pending_verify_id.clone(), }), ); - let (actual_received_amount, gas_fee) = - self.check_withdraw_rbf_psbt_valid(original_tx_btc_pending_info, &withdraw_rbf_psbt); + + let full_subsidy_amount = self + .internal_unwrap_btc_pending_info(&original_btc_pending_verify_id) + .get_subsidize_amount(); + btc_pending_info.transfer_amount += full_subsidy_amount; + + let (actual_received_amount, gas_fee) = self.check_withdraw_rbf_psbt_valid( + original_tx_btc_pending_info, + &withdraw_rbf_psbt, + full_subsidy_amount, + ); + btc_pending_info.gas_fee = gas_fee; btc_pending_info.actual_received_amount = actual_received_amount; btc_pending_info.burn_amount = actual_received_amount + gas_fee; Self::check_withdraw_chain_specific(original_tx_btc_pending_info, gas_fee); - + self.internal_unwrap_mut_btc_pending_info(&original_btc_pending_verify_id) .update_max_gas_fee(gas_fee); self.set_rbf_pending_info( diff --git a/contracts/satoshi-bridge/src/upgrade.rs b/contracts/satoshi-bridge/src/upgrade.rs index 755649c..72db212 100644 --- a/contracts/satoshi-bridge/src/upgrade.rs +++ b/contracts/satoshi-bridge/src/upgrade.rs @@ -12,6 +12,7 @@ impl Contract { VersionedContractData::V0(data) => VersionedContractData::Current(data.into()), VersionedContractData::V1(data) => VersionedContractData::Current(data.into()), VersionedContractData::V2(data) => VersionedContractData::Current(data.into()), + VersionedContractData::V3(data) => VersionedContractData::Current(data.into()), VersionedContractData::Current(data) => VersionedContractData::Current(data), }; contract diff --git a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs index 6cf303b..6507dac 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs @@ -195,4 +195,14 @@ impl Contract { self.internal_config(), ) } + + pub(crate) fn rbf_subsidize_chain_specific( + &mut self, + amount: u128, + sender_id: AccountId, + pending_tx_id: String, + output: Vec, + ) -> PromiseOrValue { + unimplemented!("This function is not supported yet"); + } }