perpl_sdk/state/fee.rs
1//! Keyed, tiered trading fee schedules.
2//!
3//! Since v1.1.7.4 fees are not a per-perpetual scalar pair but a *keyed
4//! schedule*: eight `(taker, maker)` rates indexed by an account's fee tier.
5//! Which schedule applies to a fill is selected by the perpetual's
6//! [`FeeScheduleKey`] (exchange-wide default / RWA default / the perpetual's
7//! own custom schedule); within it, the account's
8//! [`crate::state::Account::fee_tier`] picks the tier, tier 0 being the base
9//! rate.
10//!
11//! Both sides are resolved at fill time from the perpetual's current key and
12//! the account's current tier - never snapshotted at order placement - so a
13//! schedule, key or tier change takes effect on the next fill.
14//!
15//! The schedules themselves live exchange-wide in the [`FeeScheduleRegistry`],
16//! independently of which contract points at which: rewriting a schedule
17//! (`FeeScheduleSet`) and repointing a contract at one (`PerpFeeSchedIdSet`)
18//! are separate operations on the contract and are kept separate here.
19
20use fastnum::UD64;
21
22use super::*;
23
24/// Number of fee tiers in a fee schedule.
25pub const FEE_TIERS: usize = 8;
26
27/// Raw id of the exchange-wide default schedule
28/// (`C._DEFAULT_PERP_FEE_SCHED_ID`).
29const DEFAULT_FEE_KEY: u32 = 1021;
30
31/// Raw id of the exchange-wide RWA default schedule
32/// (`C._DEFAULT_RWA_FEE_SCHED_ID`).
33const DEFAULT_RWA_FEE_KEY: u32 = 1022;
34
35/// Selects the fee schedule a perpetual's fees are resolved from.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37pub enum FeeScheduleKey {
38 /// Exchange-wide default schedule, shared by every perpetual that has not
39 /// been repointed. Kept up to date by `DefaultPerpFeeScheduleSet`.
40 Default,
41
42 /// Exchange-wide default schedule for real-world assets. Kept up to date by
43 /// `DefaultRwaFeeScheduleSet`.
44 RwaDefault,
45
46 /// A perpetual's own custom schedule, keyed by its ID. Kept up to date by
47 /// `FeeScheduleSet` under that id.
48 ///
49 /// Existing under a perpetual's id does not mean the perpetual resolves its
50 /// fees from it - only `PerpFeeSchedIdSet` points a perpetual at a
51 /// schedule, and nothing stops one perpetual from being pointed at
52 /// another's.
53 Custom(types::PerpetualId),
54}
55
56impl FeeScheduleKey {
57 /// Interprets the raw on-chain schedule key.
58 pub fn from_raw(key: U256) -> Self {
59 match key.to::<u32>() {
60 DEFAULT_FEE_KEY => Self::Default,
61 DEFAULT_RWA_FEE_KEY => Self::RwaDefault,
62 perp_id => Self::Custom(perp_id),
63 }
64 }
65
66 /// Raw on-chain schedule key.
67 pub fn to_raw(self) -> U256 {
68 match self {
69 Self::Default => U256::from(DEFAULT_FEE_KEY),
70 Self::RwaDefault => U256::from(DEFAULT_RWA_FEE_KEY),
71 Self::Custom(perp_id) => U256::from(perp_id),
72 }
73 }
74}
75
76impl std::fmt::Display for FeeScheduleKey {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Default => write!(f, "default"),
80 Self::RwaDefault => write!(f, "rwa"),
81 Self::Custom(perp_id) => write!(f, "custom #{perp_id}"),
82 }
83 }
84}
85
86/// Fee schedule: a `(taker, maker)` fee pair per fee tier, with the key
87/// identifying which schedule it is.
88///
89/// Fees are fractions of the traded amount, converted from the on-chain integer
90/// representation: hundred-thousandths (`Per100K`) before contract v1.1.7.5 and
91/// millionths (ppm) from it, resolved by
92/// [`crate::state::ContractFeatures::fee_rate_converter`]. The decimal
93/// fractions here are unit-free, so a consumer never has to know which applied.
94#[derive(Clone, Copy)]
95pub struct FeeSchedule {
96 key: FeeScheduleKey,
97 tiered: bool,
98 taker_fees: [UD64; FEE_TIERS],
99 maker_fees: [UD64; FEE_TIERS],
100}
101
102impl FeeSchedule {
103 /// Builds a schedule from the raw on-chain rates.
104 ///
105 /// `fee_converter` carries the unit those integers are in, which the
106 /// deployed contract version decides -- see
107 /// [`crate::state::ContractFeatures::fee_rate_converter`]. Passing the
108 /// wrong one misreports every rate by a factor of ten, so it is
109 /// threaded in rather than assumed here.
110 pub(crate) fn new(
111 key: FeeScheduleKey,
112 taker_fees: [U256; FEE_TIERS],
113 maker_fees: [U256; FEE_TIERS],
114 fee_converter: num::Converter,
115 ) -> Self {
116 Self {
117 key,
118 tiered: true,
119 taker_fees: taker_fees.map(|fee| fee_converter.from_unsigned(fee)),
120 maker_fees: maker_fees.map(|fee| fee_converter.from_unsigned(fee)),
121 }
122 }
123
124 /// Builds a flat schedule with the same base rates in every tier.
125 ///
126 /// Used where only the base rates are observable - the `ContractAdded` and
127 /// the deprecated `MakerFeeUpdated`/`TakerFeeUpdated` events report the
128 /// tier-0 rate only.
129 pub(crate) fn flat(key: FeeScheduleKey, taker_fee: UD64, maker_fee: UD64) -> Self {
130 Self {
131 key,
132 tiered: false,
133 taker_fees: [taker_fee; FEE_TIERS],
134 maker_fees: [maker_fee; FEE_TIERS],
135 }
136 }
137
138 /// Schedule this perpetual/exchange resolves its fees from.
139 pub fn key(&self) -> FeeScheduleKey { self.key }
140
141 /// Whether the rates were reported per tier, rather than filled in from a
142 /// base rate observed on its own.
143 ///
144 /// False against a pre-v1.1.7.4 deployment, which has no tiers to report:
145 /// every tier then carries the base rate, and reading a discounted one back
146 /// tells the caller nothing the base rate did not.
147 pub fn is_tiered(&self) -> bool { self.tiered }
148
149 /// Taker fee of the given tier.
150 ///
151 /// Out-of-range tiers (the contract bounds them to `0..8` on write) resolve
152 /// to the base rate.
153 pub fn taker_fee(&self, tier: types::FeeTier) -> UD64 {
154 self.taker_fees
155 .get(tier as usize)
156 .copied()
157 .unwrap_or(self.taker_fees[0])
158 }
159
160 /// Maker fee of the given tier.
161 ///
162 /// Out-of-range tiers (the contract bounds them to `0..8` on write) resolve
163 /// to the base rate.
164 pub fn maker_fee(&self, tier: types::FeeTier) -> UD64 {
165 self.maker_fees
166 .get(tier as usize)
167 .copied()
168 .unwrap_or(self.maker_fees[0])
169 }
170
171 /// Base (tier 0) taker fee.
172 pub fn base_taker_fee(&self) -> UD64 { self.taker_fees[0] }
173
174 /// Base (tier 0) maker fee.
175 pub fn base_maker_fee(&self) -> UD64 { self.maker_fees[0] }
176
177 /// Taker fee of every tier, base rate first.
178 pub fn taker_fees(&self) -> &[UD64; FEE_TIERS] { &self.taker_fees }
179
180 /// Maker fee of every tier, base rate first.
181 pub fn maker_fees(&self) -> &[UD64; FEE_TIERS] { &self.maker_fees }
182
183 /// Same rates under a different key, for a perpetual repointed by
184 /// `PerpFeeSchedIdSet`.
185 pub(crate) fn with_key(&self, key: FeeScheduleKey) -> Self { Self { key, ..*self } }
186
187 /// Overrides the base (tier 0) rates, leaving the discounted tiers intact.
188 ///
189 /// Only the deprecated `MakerFeeUpdated`/`TakerFeeUpdated` events (replayed
190 /// from pre-v1.1.7.4 history) report a tier-0-only change.
191 pub(crate) fn with_base_taker_fee(&self, taker_fee: UD64) -> Self {
192 let mut taker_fees = self.taker_fees;
193 taker_fees[0] = taker_fee;
194 Self { taker_fees, ..*self }
195 }
196
197 /// Overrides the base (tier 0) rates, leaving the discounted tiers intact.
198 pub(crate) fn with_base_maker_fee(&self, maker_fee: UD64) -> Self {
199 let mut maker_fees = self.maker_fees;
200 maker_fees[0] = maker_fee;
201 Self { maker_fees, ..*self }
202 }
203}
204
205impl std::fmt::Debug for FeeSchedule {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 write!(f, "FeeSchedule {{ key: {}, tiers: [", self.key)?;
208 for tier in 0..FEE_TIERS {
209 write!(
210 f,
211 "{}{}/{}",
212 if tier > 0 { " " } else { "" },
213 self.taker_fees[tier],
214 self.maker_fees[tier],
215 )?;
216 }
217 write!(f, "] }}")
218 }
219}
220
221/// Every fee schedule the exchange resolves fees from, keyed by
222/// [`FeeScheduleKey`].
223///
224/// Populated on snapshotting with the two exchange-wide schedules
225/// ([`FeeScheduleKey::Default`] and [`FeeScheduleKey::RwaDefault`]) plus the
226/// custom schedule of every perpetual the snapshot tracks, then kept up to date
227/// by `FeeScheduleSet` / `DefaultPerpFeeScheduleSet` /
228/// `DefaultRwaFeeScheduleSet`.
229///
230/// The registry holds the *rates*; which schedule a perpetual resolves its fees
231/// from is the perpetual's own state (the key of
232/// [`crate::state::Perpetual::fee_schedule`]), moved only by
233/// `PerpFeeSchedIdSet`. Rewriting a schedule therefore reaches a perpetual only
234/// if that perpetual is currently pointing at it.
235#[derive(Clone, Debug)]
236pub struct FeeScheduleRegistry {
237 default: FeeSchedule,
238 rwa_default: FeeSchedule,
239 custom: HashMap<types::PerpetualId, FeeSchedule>,
240}
241
242impl FeeScheduleRegistry {
243 pub(crate) fn new(
244 default: FeeSchedule,
245 rwa_default: FeeSchedule,
246 custom: HashMap<types::PerpetualId, FeeSchedule>,
247 ) -> Self {
248 Self { default, rwa_default, custom }
249 }
250
251 /// Exchange-wide default schedule, shared by every perpetual contract that
252 /// has not been repointed at another one.
253 pub fn default_schedule(&self) -> FeeSchedule { self.default }
254
255 /// Exchange-wide default schedule for real-world assets.
256 pub fn rwa_default_schedule(&self) -> FeeSchedule { self.rwa_default }
257
258 /// Custom schedules, by the id of the perpetual contract each is keyed by.
259 ///
260 /// Covers the perpetuals known when the snapshot was built, plus any picked
261 /// up from a `FeeScheduleSet` since; a perpetual listed after the snapshot
262 /// appears here only once its own schedule is written.
263 pub fn custom_schedules(&self) -> &HashMap<types::PerpetualId, FeeSchedule> { &self.custom }
264
265 /// Every registered schedule: the exchange-wide default, the RWA default,
266 /// then the custom ones ordered by the perpetual id each is keyed by.
267 pub fn schedules(&self) -> impl Iterator<Item = FeeSchedule> {
268 [self.default, self.rwa_default].into_iter().chain(
269 self.custom
270 .keys()
271 .sorted()
272 .map(|perp_id| self.custom[perp_id]),
273 )
274 }
275
276 /// Schedule registered under the given key, `None` for a custom schedule
277 /// that has never been observed.
278 pub fn get(&self, key: FeeScheduleKey) -> Option<FeeSchedule> {
279 match key {
280 FeeScheduleKey::Default => Some(self.default),
281 FeeScheduleKey::RwaDefault => Some(self.rwa_default),
282 FeeScheduleKey::Custom(perp_id) => self.custom.get(&perp_id).copied(),
283 }
284 }
285
286 /// Registers a schedule under its own key, replacing the one held before.
287 pub(crate) fn set(&mut self, schedule: FeeSchedule) {
288 match schedule.key() {
289 FeeScheduleKey::Default => self.default = schedule,
290 FeeScheduleKey::RwaDefault => self.rwa_default = schedule,
291 FeeScheduleKey::Custom(perp_id) => {
292 self.custom.insert(perp_id, schedule);
293 },
294 }
295 }
296}
297
298impl std::fmt::Display for FeeSchedule {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 if f.alternate() {
301 // Full schedule, tier by tier
302 write!(f, "{}: ", self.key)?;
303 for tier in 0..FEE_TIERS {
304 write!(
305 f,
306 "{}{}/{}",
307 if tier > 0 { " | " } else { "" },
308 self.taker_fees[tier],
309 self.maker_fees[tier],
310 )?;
311 }
312 Ok(())
313 } else {
314 // Base rates only
315 write!(f, "{} / {} ({})", self.base_taker_fee(), self.base_maker_fee(), self.key)
316 }
317 }
318}