Skip to main content

fynd_core/encoding/
router_fees.rs

1//! Router fee configuration mirrored from the on-chain `FeeCalculator` contract.
2//!
3//! [`RouterFees`] holds the default router fees, the per-client rates (already resolved
4//! against the defaults), and the contract's fee-unit precision scale. The encoder reads a
5//! [`SharedRouterFees`] snapshot on every encode; a background
6//! [`RouterFeeFetcher`](crate::encoding::fee_fetcher::RouterFeeFetcher) refreshes it from
7//! chain, so swapping in a FeeCalculator with a different precision is tracked automatically.
8
9use std::{
10    collections::HashMap,
11    sync::{Arc, RwLock},
12};
13
14use tycho_simulation::tycho_common::Bytes;
15
16/// Legacy basis-points denominator: client fees on Fynd's API use 10,000 = 100%.
17///
18/// The router takes `clientFeeBps` in the FeeCalculator's own fee units, so the encoder scales
19/// the API value by `max_fee_units / LEGACY_BPS_DENOMINATOR` before putting it in calldata.
20pub const LEGACY_BPS_DENOMINATOR: u64 = 10_000;
21
22/// Fee-unit precision for the [`RouterFees::fallback`] configuration: 100% = 100,000,000 fee
23/// units, matching the precision the on-chain FeeCalculator reports in production.
24const FALLBACK_MAX_FEE_UNITS: u64 = 100_000_000;
25
26/// Fallback router fee on swap output, used until the on-chain FeeCalculator has been read and
27/// whenever a fetch fails: 0.1 bps (0.001%), i.e. 1000 fee units at 1e8 precision.
28const FALLBACK_FEE_ON_OUTPUT: u32 = 1_000;
29
30/// Effective router fee rates for one client, together with the precision scale they are
31/// expressed in.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct FeeRates {
34    on_output: u32,
35    on_client_fee: u32,
36    max_fee_units: u64,
37}
38
39impl FeeRates {
40    /// Creates fee rates expressed in the given fee-unit scale (`max_fee_units` = 100%).
41    pub fn new(on_output: u32, on_client_fee: u32, max_fee_units: u64) -> Self {
42        Self { on_output, on_client_fee, max_fee_units }
43    }
44
45    /// Router fee charged on the swap output, in fee units.
46    pub fn on_output(&self) -> u32 {
47        self.on_output
48    }
49
50    /// Router share of the client fee, in fee units.
51    pub fn on_client_fee(&self) -> u32 {
52        self.on_client_fee
53    }
54
55    /// Fee units representing 100% (the contract's `MAX_BPS`).
56    pub fn max_fee_units(&self) -> u64 {
57        self.max_fee_units
58    }
59
60    /// Factor converting a legacy basis-point fee into fee units
61    /// (`max_fee_units / LEGACY_BPS_DENOMINATOR`).
62    pub fn fee_units_per_bps(&self) -> u64 {
63        self.max_fee_units / LEGACY_BPS_DENOMINATOR
64    }
65
66    /// Converts a client fee given in legacy basis points into the fee units the router
67    /// expects in `ClientFeeParams.clientFeeBps`.
68    pub fn client_fee_units(&self, bps: u16) -> u64 {
69        bps as u64 * self.fee_units_per_bps()
70    }
71
72    /// Combined denominator when two fee-unit rates are multiplied (`max_fee_units`²).
73    pub fn max_fee_units_squared(&self) -> u128 {
74        (self.max_fee_units as u128) * (self.max_fee_units as u128)
75    }
76}
77
78/// Router fee configuration: precision scale, default rates, and per-client overrides.
79///
80/// Mirrors the on-chain FeeCalculator state. Rates are in fee units where
81/// [`max_fee_units`](Self::max_fee_units) represents 100%.
82#[derive(Debug, Clone)]
83pub struct RouterFees {
84    max_fee_units: u64,
85    default_fee_on_output: u32,
86    default_fee_on_client_fee: u32,
87    /// Per-client resolved `(fee_on_output, fee_on_client_fee)` in fee units. The fetcher
88    /// has already applied each client's overrides over the defaults, so a lookup miss simply
89    /// falls back to the defaults.
90    custom_fees: HashMap<Bytes, (u32, u32)>,
91}
92
93impl RouterFees {
94    /// Creates a fee configuration from on-chain values. `custom_fees` maps a client to its
95    /// resolved `(fee_on_output, fee_on_client_fee)` pair in fee units.
96    pub fn new(
97        max_fee_units: u64,
98        default_fee_on_output: u32,
99        default_fee_on_client_fee: u32,
100        custom_fees: HashMap<Bytes, (u32, u32)>,
101    ) -> Self {
102        Self { max_fee_units, default_fee_on_output, default_fee_on_client_fee, custom_fees }
103    }
104
105    /// Conservative fallback used until the on-chain FeeCalculator has been read, and whenever
106    /// a fetch fails: a 0.1 bps router fee on output, no fee on client fees, and no per-client
107    /// overrides. Lets the encoder always produce a transaction rather than failing.
108    pub fn fallback() -> Self {
109        Self::new(FALLBACK_MAX_FEE_UNITS, FALLBACK_FEE_ON_OUTPUT, 0, HashMap::new())
110    }
111
112    /// Fee units representing 100% (the contract's `MAX_BPS`).
113    pub fn max_fee_units(&self) -> u64 {
114        self.max_fee_units
115    }
116
117    /// Resolves the effective fee rates for `client`: the per-client pair when present,
118    /// otherwise the defaults. The fetcher has already applied `FeeCalculator._getFeeInfo`'s
119    /// override-or-default logic per field, so this is a plain lookup. The contract's
120    /// precision scale travels with the rates.
121    pub fn fees_for(&self, client: &Bytes) -> FeeRates {
122        let (on_output, on_client_fee) = self
123            .custom_fees
124            .get(client)
125            .copied()
126            .unwrap_or((self.default_fee_on_output, self.default_fee_on_client_fee));
127        FeeRates::new(on_output, on_client_fee, self.max_fee_units)
128    }
129
130    /// Number of clients with at least one custom fee override.
131    pub fn custom_client_count(&self) -> usize {
132        self.custom_fees.len()
133    }
134}
135
136/// Cloneable handle to the router fee configuration shared between the encoder (reader)
137/// and the background fee fetcher (writer).
138///
139/// Initialised with [`RouterFees::fallback`] so the encoder always has a usable configuration;
140/// the [`RouterFeeFetcher`](crate::encoding::fee_fetcher::RouterFeeFetcher) overwrites it with
141/// on-chain values on each successful refresh.
142#[derive(Debug, Clone)]
143pub struct SharedRouterFees(Arc<RwLock<RouterFees>>);
144
145impl Default for SharedRouterFees {
146    fn default() -> Self {
147        Self(Arc::new(RwLock::new(RouterFees::fallback())))
148    }
149}
150
151impl SharedRouterFees {
152    /// Returns a copy of the current fee configuration.
153    pub fn snapshot(&self) -> RouterFees {
154        self.0
155            .read()
156            .expect("router fees lock poisoned")
157            .clone()
158    }
159
160    /// Replaces the fee configuration with freshly fetched on-chain values.
161    pub fn set(&self, fees: RouterFees) {
162        *self
163            .0
164            .write()
165            .expect("router fees lock poisoned") = fees;
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    const SCALE: u64 = 100_000_000;
174
175    fn client(byte: u8) -> Bytes {
176        Bytes::from(vec![byte; 20])
177    }
178
179    #[test]
180    fn test_fees_for_unknown_client() {
181        let fees = RouterFees::new(SCALE, 100_000, 20_000_000, HashMap::new());
182
183        assert_eq!(fees.fees_for(&client(0xAA)), FeeRates::new(100_000, 20_000_000, SCALE));
184    }
185
186    #[test]
187    fn test_fees_for_known_client() {
188        let custom = HashMap::from([(client(0xAA), (50_000u32, 10_000_000u32))]);
189        let fees = RouterFees::new(SCALE, 100_000, 20_000_000, custom);
190
191        // Known client gets its stored pair; everyone else gets the defaults.
192        assert_eq!(fees.fees_for(&client(0xAA)), FeeRates::new(50_000, 10_000_000, SCALE));
193        assert_eq!(fees.fees_for(&client(0xBB)), FeeRates::new(100_000, 20_000_000, SCALE));
194    }
195
196    #[test]
197    fn test_fallback_is_point_one_bps_on_output() {
198        let rates = RouterFees::fallback().fees_for(&client(0xAA));
199        // 1000 / 1e8 = 0.00001 = 0.1 bps, with no fee on client fees.
200        assert_eq!(rates.on_output(), 1_000);
201        assert_eq!(rates.on_client_fee(), 0);
202        assert_eq!(rates.max_fee_units(), 100_000_000);
203    }
204
205    #[test]
206    fn test_shared_router_fees_set_overrides() {
207        let shared = SharedRouterFees::default();
208        // Defaults to the fallback before any on-chain fetch lands.
209        assert_eq!(
210            shared
211                .snapshot()
212                .fees_for(&client(0xAA))
213                .on_output(),
214            1_000
215        );
216
217        shared.set(RouterFees::new(SCALE, 1, 2, HashMap::new()));
218
219        let snapshot = shared.snapshot();
220        assert_eq!(snapshot.max_fee_units(), SCALE);
221        assert_eq!(snapshot.fees_for(&client(0xAA)), FeeRates::new(1, 2, SCALE));
222    }
223}