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
90/// hundred-thousandths (`Per100K`) representation.
91#[derive(Clone, Copy)]
92pub struct FeeSchedule {
93    key: FeeScheduleKey,
94    tiered: bool,
95    taker_fees: [UD64; FEE_TIERS],
96    maker_fees: [UD64; FEE_TIERS],
97}
98
99impl FeeSchedule {
100    /// Builds a schedule from the on-chain `Per100K` rates.
101    pub(crate) fn new(
102        key: FeeScheduleKey,
103        taker_fees_per_100k: [U256; FEE_TIERS],
104        maker_fees_per_100k: [U256; FEE_TIERS],
105        fee_converter: num::Converter,
106    ) -> Self {
107        Self {
108            key,
109            tiered: true,
110            taker_fees: taker_fees_per_100k.map(|fee| fee_converter.from_unsigned(fee)),
111            maker_fees: maker_fees_per_100k.map(|fee| fee_converter.from_unsigned(fee)),
112        }
113    }
114
115    /// Builds a flat schedule with the same base rates in every tier.
116    ///
117    /// Used where only the base rates are observable - the `ContractAdded` and
118    /// the deprecated `MakerFeeUpdated`/`TakerFeeUpdated` events report the
119    /// tier-0 rate only.
120    pub(crate) fn flat(key: FeeScheduleKey, taker_fee: UD64, maker_fee: UD64) -> Self {
121        Self {
122            key,
123            tiered: false,
124            taker_fees: [taker_fee; FEE_TIERS],
125            maker_fees: [maker_fee; FEE_TIERS],
126        }
127    }
128
129    /// Schedule this perpetual/exchange resolves its fees from.
130    pub fn key(&self) -> FeeScheduleKey { self.key }
131
132    /// Whether the rates were reported per tier, rather than filled in from a
133    /// base rate observed on its own.
134    ///
135    /// False against a pre-v1.1.7.4 deployment, which has no tiers to report:
136    /// every tier then carries the base rate, and reading a discounted one back
137    /// tells the caller nothing the base rate did not.
138    pub fn is_tiered(&self) -> bool { self.tiered }
139
140    /// Taker fee of the given tier.
141    ///
142    /// Out-of-range tiers (the contract bounds them to `0..8` on write) resolve
143    /// to the base rate.
144    pub fn taker_fee(&self, tier: types::FeeTier) -> UD64 {
145        self.taker_fees
146            .get(tier as usize)
147            .copied()
148            .unwrap_or(self.taker_fees[0])
149    }
150
151    /// Maker fee of the given tier.
152    ///
153    /// Out-of-range tiers (the contract bounds them to `0..8` on write) resolve
154    /// to the base rate.
155    pub fn maker_fee(&self, tier: types::FeeTier) -> UD64 {
156        self.maker_fees
157            .get(tier as usize)
158            .copied()
159            .unwrap_or(self.maker_fees[0])
160    }
161
162    /// Base (tier 0) taker fee.
163    pub fn base_taker_fee(&self) -> UD64 { self.taker_fees[0] }
164
165    /// Base (tier 0) maker fee.
166    pub fn base_maker_fee(&self) -> UD64 { self.maker_fees[0] }
167
168    /// Taker fee of every tier, base rate first.
169    pub fn taker_fees(&self) -> &[UD64; FEE_TIERS] { &self.taker_fees }
170
171    /// Maker fee of every tier, base rate first.
172    pub fn maker_fees(&self) -> &[UD64; FEE_TIERS] { &self.maker_fees }
173
174    /// Same rates under a different key, for a perpetual repointed by
175    /// `PerpFeeSchedIdSet`.
176    pub(crate) fn with_key(&self, key: FeeScheduleKey) -> Self { Self { key, ..*self } }
177
178    /// Overrides the base (tier 0) rates, leaving the discounted tiers intact.
179    ///
180    /// Only the deprecated `MakerFeeUpdated`/`TakerFeeUpdated` events (replayed
181    /// from pre-v1.1.7.4 history) report a tier-0-only change.
182    pub(crate) fn with_base_taker_fee(&self, taker_fee: UD64) -> Self {
183        let mut taker_fees = self.taker_fees;
184        taker_fees[0] = taker_fee;
185        Self { taker_fees, ..*self }
186    }
187
188    /// Overrides the base (tier 0) rates, leaving the discounted tiers intact.
189    pub(crate) fn with_base_maker_fee(&self, maker_fee: UD64) -> Self {
190        let mut maker_fees = self.maker_fees;
191        maker_fees[0] = maker_fee;
192        Self { maker_fees, ..*self }
193    }
194}
195
196impl std::fmt::Debug for FeeSchedule {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        write!(f, "FeeSchedule {{ key: {}, tiers: [", self.key)?;
199        for tier in 0..FEE_TIERS {
200            write!(
201                f,
202                "{}{}/{}",
203                if tier > 0 { " " } else { "" },
204                self.taker_fees[tier],
205                self.maker_fees[tier],
206            )?;
207        }
208        write!(f, "] }}")
209    }
210}
211
212/// Every fee schedule the exchange resolves fees from, keyed by
213/// [`FeeScheduleKey`].
214///
215/// Populated on snapshotting with the two exchange-wide schedules
216/// ([`FeeScheduleKey::Default`] and [`FeeScheduleKey::RwaDefault`]) plus the
217/// custom schedule of every perpetual the snapshot tracks, then kept up to date
218/// by `FeeScheduleSet` / `DefaultPerpFeeScheduleSet` /
219/// `DefaultRwaFeeScheduleSet`.
220///
221/// The registry holds the *rates*; which schedule a perpetual resolves its fees
222/// from is the perpetual's own state (the key of
223/// [`crate::state::Perpetual::fee_schedule`]), moved only by
224/// `PerpFeeSchedIdSet`. Rewriting a schedule therefore reaches a perpetual only
225/// if that perpetual is currently pointing at it.
226#[derive(Clone, Debug)]
227pub struct FeeScheduleRegistry {
228    default: FeeSchedule,
229    rwa_default: FeeSchedule,
230    custom: HashMap<types::PerpetualId, FeeSchedule>,
231}
232
233impl FeeScheduleRegistry {
234    pub(crate) fn new(
235        default: FeeSchedule,
236        rwa_default: FeeSchedule,
237        custom: HashMap<types::PerpetualId, FeeSchedule>,
238    ) -> Self {
239        Self { default, rwa_default, custom }
240    }
241
242    /// Exchange-wide default schedule, shared by every perpetual contract that
243    /// has not been repointed at another one.
244    pub fn default_schedule(&self) -> FeeSchedule { self.default }
245
246    /// Exchange-wide default schedule for real-world assets.
247    pub fn rwa_default_schedule(&self) -> FeeSchedule { self.rwa_default }
248
249    /// Custom schedules, by the id of the perpetual contract each is keyed by.
250    ///
251    /// Covers the perpetuals known when the snapshot was built, plus any picked
252    /// up from a `FeeScheduleSet` since; a perpetual listed after the snapshot
253    /// appears here only once its own schedule is written.
254    pub fn custom_schedules(&self) -> &HashMap<types::PerpetualId, FeeSchedule> { &self.custom }
255
256    /// Every registered schedule: the exchange-wide default, the RWA default,
257    /// then the custom ones ordered by the perpetual id each is keyed by.
258    pub fn schedules(&self) -> impl Iterator<Item = FeeSchedule> {
259        [self.default, self.rwa_default].into_iter().chain(
260            self.custom
261                .keys()
262                .sorted()
263                .map(|perp_id| self.custom[perp_id]),
264        )
265    }
266
267    /// Schedule registered under the given key, `None` for a custom schedule
268    /// that has never been observed.
269    pub fn get(&self, key: FeeScheduleKey) -> Option<FeeSchedule> {
270        match key {
271            FeeScheduleKey::Default => Some(self.default),
272            FeeScheduleKey::RwaDefault => Some(self.rwa_default),
273            FeeScheduleKey::Custom(perp_id) => self.custom.get(&perp_id).copied(),
274        }
275    }
276
277    /// Registers a schedule under its own key, replacing the one held before.
278    pub(crate) fn set(&mut self, schedule: FeeSchedule) {
279        match schedule.key() {
280            FeeScheduleKey::Default => self.default = schedule,
281            FeeScheduleKey::RwaDefault => self.rwa_default = schedule,
282            FeeScheduleKey::Custom(perp_id) => {
283                self.custom.insert(perp_id, schedule);
284            },
285        }
286    }
287}
288
289impl std::fmt::Display for FeeSchedule {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        if f.alternate() {
292            // Full schedule, tier by tier
293            write!(f, "{}: ", self.key)?;
294            for tier in 0..FEE_TIERS {
295                write!(
296                    f,
297                    "{}{}/{}",
298                    if tier > 0 { " | " } else { "" },
299                    self.taker_fees[tier],
300                    self.maker_fees[tier],
301                )?;
302            }
303            Ok(())
304        } else {
305            // Base rates only
306            write!(f, "{} / {} ({})", self.base_taker_fee(), self.base_maker_fee(), self.key)
307        }
308    }
309}