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}