Skip to content

Commit 00a5f46

Browse files
authored
fix(ckdoge): use correct transaction fees (#7660)
Follow-up on #7360 to use the correct parameters to compute the fees involved in a transaction sent by the minter to the Dogecoin network in case of a withdrawal. The implementation introduces a new trait `FeeEstimator`, of which there are two implementations (one for Bitcoin and another one for Dogecoin), and parametrize the existing methods to build transactions by an instance of that trait. A follow-up PR will add the `estimate_withdrawal_fee` endpoint.
1 parent e86ffa1 commit 00a5f46

File tree

13 files changed

+462
-142
lines changed

13 files changed

+462
-142
lines changed

rs/bitcoin/ckbtc/agent/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use candid::{CandidType, Deserialize, Principal};
22
use ic_agent::Agent;
3-
use ic_ckbtc_minter::queries::RetrieveBtcStatusRequest;
3+
use ic_ckbtc_minter::queries::{EstimateFeeArg, RetrieveBtcStatusRequest, WithdrawalFee};
44
use ic_ckbtc_minter::state::RetrieveBtcStatus;
55
use ic_ckbtc_minter::state::eventlog::{Event, GetEventsArg};
66
use ic_ckbtc_minter::updates::{
@@ -114,6 +114,19 @@ impl CkBtcMinterAgent {
114114
.await
115115
}
116116

117+
pub async fn estimate_withdrawal_fee(
118+
&self,
119+
amount: u64,
120+
) -> Result<WithdrawalFee, CkBtcMinterAgentError> {
121+
self.query(
122+
"estimate_withdrawal_fee",
123+
EstimateFeeArg {
124+
amount: Some(amount),
125+
},
126+
)
127+
.await
128+
}
129+
117130
pub async fn distribute_kyt_fee(&self) -> Result<(), CkBtcMinterAgentError> {
118131
self.update("distribute_kyt_fee", ()).await
119132
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use crate::state::CkBtcMinterState;
2+
use crate::tx::UnsignedTransaction;
3+
use crate::{Network, fake_sign};
4+
use ic_btc_interface::{MillisatoshiPerByte, Satoshi};
5+
use std::cmp::max;
6+
7+
pub trait FeeEstimator {
8+
const DUST_LIMIT: u64;
9+
10+
/// Estimate the median fees based on the given fee percentiles (slice of fee rates in milli base unit per vbyte/byte).
11+
fn estimate_median_fee(
12+
&self,
13+
fee_percentiles: &[MillisatoshiPerByte],
14+
) -> Option<MillisatoshiPerByte>;
15+
16+
/// Evaluate the fee necessary to cover the minter's cycles consumption.
17+
fn evaluate_minter_fee(&self, num_inputs: u64, num_outputs: u64) -> Satoshi;
18+
19+
/// Evaluate transaction fee with the given fee rate (in milli base unit per vbyte/byte)
20+
fn evaluate_transaction_fee(&self, tx: &UnsignedTransaction, fee_rate: u64) -> u64;
21+
22+
/// Compute a new minimum withdrawal amount based on the current fee rate
23+
fn fee_based_minimum_withdrawal_amount(&self, median_fee: MillisatoshiPerByte) -> Satoshi;
24+
}
25+
26+
pub struct BitcoinFeeEstimator {
27+
/// The Bitcoin network that the minter will connect to
28+
network: Network,
29+
/// Minimum amount of bitcoin that can be retrieved
30+
retrieve_btc_min_amount: u64,
31+
/// The fee for a single Bitcoin check request.
32+
check_fee: u64,
33+
}
34+
35+
impl BitcoinFeeEstimator {
36+
/// The minter's address is of type P2WPKH which means it has a dust limit of 294 sats.
37+
/// For additional safety, we round that value up.
38+
pub const MINTER_ADDRESS_P2PWPKH_DUST_LIMIT: Satoshi = 300;
39+
40+
pub fn new(network: Network, retrieve_btc_min_amount: u64, check_fee: u64) -> Self {
41+
Self {
42+
network,
43+
retrieve_btc_min_amount,
44+
check_fee,
45+
}
46+
}
47+
48+
pub fn from_state(state: &CkBtcMinterState) -> Self {
49+
Self::new(
50+
state.btc_network,
51+
state.retrieve_btc_min_amount,
52+
state.check_fee,
53+
)
54+
}
55+
56+
/// An estimated fee per vbyte of 142 millisatoshis per vbyte was selected around 2025.06.21 01:09:50 UTC
57+
/// for Bitcoin Mainnet, whereas the median fee around that time should have been 2_000.
58+
/// Until we know the root cause, we ensure that the estimated fee has a meaningful minimum value.
59+
const fn minimum_fee_per_vbyte(&self) -> MillisatoshiPerByte {
60+
match &self.network {
61+
Network::Mainnet => 1_500,
62+
Network::Testnet => 1_000,
63+
Network::Regtest => 0,
64+
}
65+
}
66+
}
67+
68+
impl FeeEstimator for BitcoinFeeEstimator {
69+
// The default dustRelayFee is 3 sat/vB,
70+
// which translates to a dust threshold of 546 satoshi for P2PKH outputs.
71+
// The threshold for other types is lower,
72+
// so we simply use 546 satoshi as the minimum amount per output.
73+
const DUST_LIMIT: u64 = 546;
74+
75+
fn estimate_median_fee(
76+
&self,
77+
fee_percentiles: &[MillisatoshiPerByte],
78+
) -> Option<MillisatoshiPerByte> {
79+
/// The default fee we use on regtest networks.
80+
const DEFAULT_REGTEST_FEE: MillisatoshiPerByte = 5_000;
81+
82+
let median_fee = match &self.network {
83+
Network::Mainnet | Network::Testnet => {
84+
if fee_percentiles.len() < 100 {
85+
return None;
86+
}
87+
Some(fee_percentiles[50])
88+
}
89+
Network::Regtest => Some(DEFAULT_REGTEST_FEE),
90+
};
91+
median_fee.map(|f| f.max(self.minimum_fee_per_vbyte()))
92+
}
93+
94+
fn evaluate_minter_fee(&self, num_inputs: u64, num_outputs: u64) -> u64 {
95+
const MINTER_FEE_PER_INPUT: u64 = 146;
96+
const MINTER_FEE_PER_OUTPUT: u64 = 4;
97+
const MINTER_FEE_CONSTANT: u64 = 26;
98+
99+
max(
100+
MINTER_FEE_PER_INPUT * num_inputs
101+
+ MINTER_FEE_PER_OUTPUT * num_outputs
102+
+ MINTER_FEE_CONSTANT,
103+
Self::MINTER_ADDRESS_P2PWPKH_DUST_LIMIT,
104+
)
105+
}
106+
107+
/// Returns the minimum withdrawal amount based on the current median fee rate (in millisatoshi per byte).
108+
/// The returned amount is in satoshi.
109+
fn fee_based_minimum_withdrawal_amount(&self, median_fee: MillisatoshiPerByte) -> Satoshi {
110+
match self.network {
111+
Network::Mainnet | Network::Testnet => {
112+
const PER_REQUEST_RBF_BOUND: u64 = 22_100;
113+
const PER_REQUEST_VSIZE_BOUND: u64 = 221;
114+
const PER_REQUEST_MINTER_FEE_BOUND: u64 = 305;
115+
116+
let median_fee_rate = median_fee / 1_000;
117+
((PER_REQUEST_RBF_BOUND
118+
+ PER_REQUEST_VSIZE_BOUND * median_fee_rate
119+
+ PER_REQUEST_MINTER_FEE_BOUND
120+
+ self.check_fee)
121+
/ 50_000) //TODO DEFI-2187: adjust increment of minimum withdrawal amount to be a multiple of retrieve_btc_min_amount/2
122+
* 50_000
123+
+ self.retrieve_btc_min_amount
124+
}
125+
Network::Regtest => self.retrieve_btc_min_amount,
126+
}
127+
}
128+
129+
fn evaluate_transaction_fee(
130+
&self,
131+
unsigned_tx: &UnsignedTransaction,
132+
fee_per_vbyte: u64,
133+
) -> u64 {
134+
let tx_vsize = fake_sign(unsigned_tx).vsize();
135+
(tx_vsize as u64 * fee_per_vbyte) / 1000
136+
}
137+
}

0 commit comments

Comments
 (0)