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}