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}