|
| 1 | +//! SNIP-35 dynamic L2 gas pricing primitives. |
| 2 | +//! |
| 3 | +//! This module implements the consensus-level fee mechanism described in SNIP-35: |
| 4 | +//! - `compute_fee_actual`: median of recent `fee_proposal` values (sliding window). |
| 5 | +//! - `compute_fee_target`: USD-denominated target converted to FRI via STRK/USD oracle. |
| 6 | +//! - `compute_fee_proposal`: honest proposer's recommended fee, clamped within a margin of |
| 7 | +//! `fee_actual`. |
| 8 | +//! |
| 9 | +//! See also: `fee_market` for EIP-1559-style base-fee adjustment, which receives |
| 10 | +//! `fee_actual` as a floor. |
| 11 | +
|
| 12 | +use ethnum::U256; |
| 13 | +use starknet_api::block::GasPrice; |
| 14 | + |
| 15 | +/// Scale factor for 18-decimal fixed-point conversion (1 STRK = 10^18 FRI). |
| 16 | +const FRI_DECIMALS_SCALE: u128 = 10u128.pow(18); |
| 17 | + |
| 18 | +/// Denominator for parts-per-thousand calculations in SNIP-35 fee_proposal bounds. |
| 19 | +pub(crate) const PPT_DENOMINATOR: u128 = 1000; |
| 20 | + |
| 21 | +/// Compute fee_actual from the last `window_size` fee_proposal values (SNIP-35). |
| 22 | +/// Returns the median: for even `window_size`, the average of the two middle values rounded |
| 23 | +/// down; for odd `window_size`, the single middle value. |
| 24 | +/// Returns `None` if fewer than `window_size` proposals are available. |
| 25 | +pub fn compute_fee_actual(proposals: &[GasPrice], window_size: usize) -> Option<GasPrice> { |
| 26 | + if proposals.len() < window_size || window_size < 2 { |
| 27 | + return None; |
| 28 | + } |
| 29 | + let window = &proposals[proposals.len() - window_size..]; |
| 30 | + let mut sorted: Vec<u128> = window.iter().map(|p| p.0).collect(); |
| 31 | + sorted.sort(); |
| 32 | + let mid = window_size / 2; |
| 33 | + let median = if window_size.is_multiple_of(2) { |
| 34 | + // Even: average of the two middle values, rounded down. |
| 35 | + // Overflow-safe averaging: a + (b - a) / 2 (safe because sorted, so b >= a). |
| 36 | + sorted[mid - 1] + (sorted[mid] - sorted[mid - 1]) / 2 |
| 37 | + } else { |
| 38 | + sorted[mid] |
| 39 | + }; |
| 40 | + // Return None if median is zero (e.g., pre-SNIP-35 blocks with fee_proposal=0). |
| 41 | + // This triggers the l2_gas_price fallback in both proposer and validator paths. |
| 42 | + if median == 0 { None } else { Some(GasPrice(median)) } |
| 43 | +} |
| 44 | + |
| 45 | +/// Compute the fee target from STRK/USD price and a USD cost target (SNIP-35). |
| 46 | +/// `target_atto_usd_per_l2_gas` is in atto-USD (18-decimal fixed-point). |
| 47 | +/// `strk_usd_rate` is the STRK/USD price with 18 decimals (from oracle). |
| 48 | +/// Result is in FRI, clamped to `[floor_min_fri, floor_max_fri]`. |
| 49 | +pub fn compute_fee_target( |
| 50 | + target_atto_usd_per_l2_gas: u128, |
| 51 | + strk_usd_rate: u128, |
| 52 | + floor_min_fri: u128, |
| 53 | + floor_max_fri: u128, |
| 54 | +) -> GasPrice { |
| 55 | + if strk_usd_rate == 0 { |
| 56 | + return GasPrice(floor_max_fri); |
| 57 | + } |
| 58 | + // floor_fri = target_atto_usd_per_l2_gas * 10^18 / strk_usd_rate |
| 59 | + let numerator = U256::from(target_atto_usd_per_l2_gas) * U256::from(FRI_DECIMALS_SCALE); |
| 60 | + let floor = numerator / U256::from(strk_usd_rate); |
| 61 | + let floor_u128 = u128::try_from(floor).unwrap_or(u128::MAX); |
| 62 | + GasPrice(floor_u128.clamp(floor_min_fri, floor_max_fri)) |
| 63 | +} |
| 64 | + |
| 65 | +/// Compute the fee_proposal an honest proposer should publish (SNIP-35). |
| 66 | +/// - If oracle failed (`fee_target` is `None`): freeze at `fee_actual`. |
| 67 | +/// - Otherwise: clamp `fee_target` to within +/-`margin_ppt` parts per thousand of `fee_actual`. |
| 68 | +pub fn compute_fee_proposal( |
| 69 | + fee_target: Option<GasPrice>, |
| 70 | + fee_actual: GasPrice, |
| 71 | + margin_ppt: u128, |
| 72 | +) -> GasPrice { |
| 73 | + let Some(fee_target) = fee_target else { |
| 74 | + return fee_actual; |
| 75 | + }; |
| 76 | + let upper = fee_actual.0.saturating_mul(PPT_DENOMINATOR + margin_ppt) / PPT_DENOMINATOR; |
| 77 | + let lower = fee_actual.0.saturating_mul(PPT_DENOMINATOR) / (PPT_DENOMINATOR + margin_ppt); |
| 78 | + GasPrice(fee_target.0.clamp(lower, upper)) |
| 79 | +} |
0 commit comments