Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,601 changes: 890 additions & 711 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ BRIDGE_MANIFEST := $(MAKEFILE_DIR)/contracts/satoshi-bridge/Cargo.toml

RFLAGS="-C link-arg=-s"

FEATURES = bitcoin zcash
FEATURES = bitcoin zcash dash

release: $(addprefix build-,$(FEATURES))
$(call build_release_wasm,nbtc,nbtc)
Expand Down
5 changes: 4 additions & 1 deletion contracts/satoshi-bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "-
[package.metadata.near.reproducible_build.variant.zcash]
container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked", "--no-abi", "--no-default-features", "--features", "zcash"]

[package.metadata.near.reproducible_build.variant.dash]
container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked", "--no-abi", "--no-default-features", "--features", "dash"]

[dependencies]
near-sdk.workspace = true
near-contract-standards.workspace = true
Expand All @@ -44,7 +47,6 @@ sapling-crypto = { version = "0.5.0", default-features = false, optional = true
zcash_protocol = { version = "0.6.1" }
core2 = { version = "0.3", optional = true }
zcash_address = { version = "0.9.0" }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.12", features = ["custom"] }

Expand All @@ -59,3 +61,4 @@ bs58 = "0.5"
default = []
zcash = ["zcash_primitives", "zcash_transparent", "orchard", "core2", "sapling-crypto"]
bitcoin = []
dash = []
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::{
env, near, require, serde_json, BTCPendingInfo, Contract, ContractExt, Gas, Promise,
PromiseOrValue, MAX_BOOL_RESULT,
};
use super::assert_verification_succeeded;
use crate::{env, near, BTCPendingInfo, Contract, ContractExt, Gas, Promise, PromiseOrValue};

pub const GAS_FOR_VERIFY_ACTIVE_UTXO_MANAGEMENT_CALL_BACK: Gas = Gas::from_tgas(50);

Expand All @@ -16,15 +14,24 @@ impl Contract {
) -> Promise {
let config = self.internal_config();
let confirmations = self.get_confirmations(config, btc_pending_info.actual_received_amount);
self.verify_transaction_inclusion_promise(

#[cfg(not(feature = "dash"))]
let verify_promise = self.verify_transaction_inclusion_promise(
config.btc_light_client_account_id.clone(),
tx_id.clone(),
tx_block_blockhash,
tx_index,
merkle_proof,
confirmations,
)
.then(
);

#[cfg(feature = "dash")]
let verify_promise = {
let _ = (&tx_block_blockhash, tx_index, &merkle_proof);
self.verify_transaction_via_mpc(tx_id.clone(), confirmations)
};

verify_promise.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_VERIFY_ACTIVE_UTXO_MANAGEMENT_CALL_BACK)
.verify_active_utxo_management_callback(tx_id),
Expand All @@ -39,11 +46,7 @@ impl Contract {
&mut self,
tx_id: String,
) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
assert_verification_succeeded();
self.internal_unwrap_btc_pending_info(&tx_id)
.assert_pending_verify();
self.internal_unwrap_mut_btc_pending_info(&tx_id)
Expand Down
37 changes: 21 additions & 16 deletions contracts/satoshi-bridge/src/btc_light_client/deposit.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use near_sdk::serde_json::Value;

use super::assert_verification_succeeded;
use crate::{
burn::GAS_FOR_BURN_CALL,
env, ext_nbtc,
mint::{GAS_FOR_MINT_CALL, GAS_FOR_MINT_CALL_BACK},
near, require, serde_json, AccountId, Contract, ContractExt, DepositMsg, Event, Gas, NearToken,
PendingUTXOInfo, PostAction, Promise, PromiseOrValue, SafeDepositMsg, MAX_BOOL_RESULT,
PendingUTXOInfo, PostAction, Promise, PromiseOrValue, SafeDepositMsg,
MAX_FT_TRANSFER_CALL_RESULT, U128,
};

Expand All @@ -29,6 +30,8 @@ impl Contract {
} else {
self.get_extra_msg_confirmations(config, deposit_amount)
};

#[cfg(not(feature = "dash"))]
let promise = self.verify_transaction_inclusion_promise(
config.btc_light_client_account_id.clone(),
pending_utxo_info.tx_id.clone(),
Expand All @@ -38,6 +41,12 @@ impl Contract {
confirmations,
);

#[cfg(feature = "dash")]
let promise = {
let _ = (&tx_block_blockhash, tx_index, &merkle_proof);
self.verify_transaction_via_mpc(pending_utxo_info.tx_id.clone(), confirmations)
};

if deposit_amount < config.min_deposit_amount {
promise.then(
Self::ext(env::current_account_id())
Expand Down Expand Up @@ -80,6 +89,8 @@ impl Contract {
) -> Promise {
let config = self.internal_config();
let confirmations = self.get_confirmations(config, deposit_amount);

#[cfg(not(feature = "dash"))]
let promise = self.verify_transaction_inclusion_promise(
config.btc_light_client_account_id.clone(),
pending_utxo_info.tx_id.clone(),
Expand All @@ -89,6 +100,12 @@ impl Contract {
confirmations,
);

#[cfg(feature = "dash")]
let promise = {
let _ = (&tx_block_blockhash, tx_index, &merkle_proof);
self.verify_transaction_via_mpc(pending_utxo_info.tx_id.clone(), confirmations)
};

if deposit_amount < config.min_deposit_amount {
promise.then(
Self::ext(env::current_account_id())
Expand Down Expand Up @@ -118,11 +135,7 @@ impl Contract {
recipient_id: AccountId,
pending_utxo_info: PendingUTXOInfo,
) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
assert_verification_succeeded();
require!(
self.data_mut()
.verified_deposit_utxo
Expand Down Expand Up @@ -153,11 +166,7 @@ impl Contract {
pending_utxo_info: PendingUTXOInfo,
post_actions: Option<Vec<PostAction>>,
) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
assert_verification_succeeded();
require!(
self.data_mut()
.verified_deposit_utxo
Expand All @@ -183,11 +192,7 @@ impl Contract {
msg: String,
pending_utxo_info: PendingUTXOInfo,
) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
assert_verification_succeeded();
require!(
self.data_mut()
.verified_deposit_utxo
Expand Down
33 changes: 33 additions & 0 deletions contracts/satoshi-bridge/src/btc_light_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,44 @@ use near_sdk::serde::{
};
use std::{fmt, str::FromStr};

#[cfg(not(feature = "dash"))]
use crate::require;
use crate::{env, ext_contract, near, AccountId, Contract, Gas, Promise};
pub mod active_utxo_management;
pub mod deposit;
pub mod withdraw;

/// Maximum expected byte length for the verification promise result.
/// For light client (BTC/non-dash): bool result (~10 bytes).
/// For MPC (DASH): VerifyForeignTransactionResponse (~1024 bytes).
#[cfg(not(feature = "dash"))]
pub(crate) const MAX_VERIFY_RESULT: usize = crate::MAX_BOOL_RESULT;
#[cfg(feature = "dash")]
pub(crate) const MAX_VERIFY_RESULT: usize = crate::MAX_MPC_VERIFY_RESULT;

/// Assert that the transaction verification promise (light client or MPC) succeeded.
///
/// For non-DASH (light client): parses the `bool` return value and requires it to be `true`.
/// For DASH (MPC): a successful promise means the MPC network confirmed the transaction.
/// The MPC contract's `verify_foreign_transaction` panics if verification fails,
/// so reaching the callback means the transaction is verified.
pub(crate) fn assert_verification_succeeded() {
let result_bytes =
env::promise_result_checked(0, MAX_VERIFY_RESULT).expect("Transaction verification failed");

#[cfg(not(feature = "dash"))]
{
let is_valid = crate::serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion returned invalid result");
require!(is_valid, "verify_transaction_inclusion returned false");
}

#[cfg(feature = "dash")]
{
let _ = result_bytes;
}
}

pub const GAS_FOR_VERIFY_TRANSACTION_INCLUSION: Gas = Gas::from_tgas(10);
pub const GAS_FOR_GET_LAST_BLOCK_HEIGHT: Gas = Gas::from_tgas(3);
#[near(serializers = [borsh])]
Expand Down
27 changes: 15 additions & 12 deletions contracts/satoshi-bridge/src/btc_light_client/withdraw.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::{
env, near, require, serde_json, BTCPendingInfo, Contract, ContractExt, Gas, Promise,
PromiseOrValue, MAX_BOOL_RESULT,
};
use super::assert_verification_succeeded;
use crate::{env, near, BTCPendingInfo, Contract, ContractExt, Gas, Promise, PromiseOrValue};

pub const GAS_FOR_VERIFY_WITHDRAW_CALL_BACK: Gas = Gas::from_tgas(50);
pub const GAS_FOR_VERIFY_CANCEL_WITHDRAW_CALL_BACK: Gas = Gas::from_tgas(50);
Expand All @@ -17,15 +15,24 @@ impl Contract {
) -> Promise {
let config = self.internal_config();
let confirmations = self.get_confirmations(config, btc_pending_info.actual_received_amount);
self.verify_transaction_inclusion_promise(

#[cfg(not(feature = "dash"))]
let verify_promise = self.verify_transaction_inclusion_promise(
config.btc_light_client_account_id.clone(),
tx_id.clone(),
tx_block_blockhash,
tx_index,
merkle_proof,
confirmations,
)
.then(
);

#[cfg(feature = "dash")]
let verify_promise = {
let _ = (&tx_block_blockhash, tx_index, &merkle_proof);
self.verify_transaction_via_mpc(tx_id.clone(), confirmations)
};

verify_promise.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_VERIFY_CANCEL_WITHDRAW_CALL_BACK)
.internal_verify_withdraw_callback(tx_id),
Expand All @@ -37,11 +44,7 @@ impl Contract {
impl Contract {
#[private]
pub fn internal_verify_withdraw_callback(&mut self, tx_id: String) -> PromiseOrValue<bool> {
let result_bytes = env::promise_result_checked(0, MAX_BOOL_RESULT)
.expect("Call verify_transaction_inclusion failed");
let is_valid = serde_json::from_slice::<bool>(&result_bytes)
.expect("verify_transaction_inclusion return not bool");
require!(is_valid, "verify_transaction_inclusion return false");
assert_verification_succeeded();
self.internal_unwrap_btc_pending_info(&tx_id)
.assert_pending_verify();
self.internal_unwrap_mut_btc_pending_info(&tx_id)
Expand Down
117 changes: 117 additions & 0 deletions contracts/satoshi-bridge/src/dash_utils/contract_methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use crate::dash_utils::types::ChainSpecificData;
use crate::env;
use crate::psbt_wrapper::PsbtWrapper;
use crate::{BTCPendingInfo, Contract, Event};
use bitcoin::{OutPoint, TxOut};
use near_sdk::json_types::U128;
use near_sdk::{require, AccountId, PromiseOrValue};

macro_rules! define_rbf_method {
($method:ident, $internal_fn:ident) => {
pub(crate) fn $method(
&mut self,
account_id: AccountId,
original_btc_pending_verify_id: String,
output: Vec<TxOut>,
_chain_specific_data: Option<ChainSpecificData>,
) {
let predecessor_account_id = env::predecessor_account_id();
let original_tx_btc_pending_info =
self.internal_unwrap_btc_pending_info(&original_btc_pending_verify_id);

let new_psbt = self.generate_psbt_from_original_psbt_and_new_output(
original_tx_btc_pending_info,
output,
);

let btc_pending_id = self.$internal_fn(
&account_id,
original_btc_pending_verify_id,
new_psbt,
predecessor_account_id,
);

self.internal_unwrap_mut_account(&account_id)
.btc_pending_sign_id = Some(btc_pending_id.clone());

Event::GenerateBtcPendingInfo {
account_id: &account_id,
btc_pending_id: &btc_pending_id,
}
.emit();
}
};
}

impl Contract {
pub(crate) fn check_psbt_chain_specific(
&self,
_psbt: &PsbtWrapper,
_gas_fee: u128,
_target_btc_address: String,
) {
}

pub(crate) fn check_withdraw_chain_specific(
original_tx_btc_pending_info: &BTCPendingInfo,
gas_fee: u128,
) {
// 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.");
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn ft_on_transfer_withdraw_chain_specific(
&mut self,
sender_id: AccountId,
amount: u128,
target_btc_address: String,
input: Vec<OutPoint>,
output: Vec<TxOut>,
max_gas_fee: Option<U128>,
_chain_specific_data: Option<ChainSpecificData>,
) -> PromiseOrValue<U128> {
self.create_btc_pending_info(
sender_id,
amount,
target_btc_address,
PsbtWrapper::new(input, output),
max_gas_fee,
);
PromiseOrValue::Value(U128(0))
}

define_rbf_method!(withdraw_rbf_chain_specific, internal_withdraw_rbf);
define_rbf_method!(cancel_withdraw_chain_specific, internal_cancel_withdraw);
define_rbf_method!(
cancel_active_utxo_management_chain_specific,
internal_cancel_active_utxo_management
);
define_rbf_method!(
active_utxo_management_rbf_chain_specific,
internal_active_utxo_management_rbf
);

pub(crate) fn active_utxo_management_chain_specific(
&mut self,
account_id: AccountId,
input: Vec<OutPoint>,
output: Vec<TxOut>,
) {
self.create_active_utxo_management_pending_info(
account_id,
PsbtWrapper::new(input, output),
);
}

pub(crate) fn generate_psbt_from_original_psbt_and_new_output(
&self,
original_tx_btc_pending_info: &BTCPendingInfo,
output: Vec<TxOut>,
) -> PsbtWrapper {
let original_psbt = original_tx_btc_pending_info.get_psbt();
PsbtWrapper::from_original_psbt(original_psbt, output)
}
}
4 changes: 4 additions & 0 deletions contracts/satoshi-bridge/src/dash_utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod contract_methods;
pub mod psbt_wrapper;
pub mod transaction;
pub mod types;
Loading
Loading