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