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}