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