Skip to main content

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}