Skip to main content

perpl_sdk/state/
version.rs

1//! Deployed contract version and the feature set derived from it.
2//!
3//! The exchange is an upgradeable proxy, so the deployed implementation can lag
4//! behind the ABI the SDK is compiled against. Since v1.1.7.4 the contract
5//! reports its own version via `getContractVersion()` and stamps
6//! `ContractVersionSet` inside the upgrade transaction, which makes capability
7//! detection authoritative rather than inferred: [`ContractFeatures::probe`]
8//! reads the version once while building a snapshot, and
9//! [`ContractFeatures::observe_version`] follows it in both directions from the
10//! event stream.
11//!
12//! Older deployments have no version getter at all, so they are detected by
13//! probing a selector added by the release in question.
14
15use alloy::{eips::BlockId, primitives::U256, providers::Provider};
16
17use crate::{abi::dex, num, types};
18
19/// Version of the deployed exchange smart contract.
20///
21/// Renders as `v1.<major>.<minor>.<patch>` - the leading `v1` epoch is fixed
22/// for this contract's lifetime and changes only with an entirely new contract.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
24pub struct ContractVersion {
25    major: u64,
26    minor: u64,
27    patch: u64,
28}
29
30impl ContractVersion {
31    /// First version exposing the V2 information getters (`getPerpetualInfoV2`,
32    /// `getPositionV2`) and the corresponding V2 position events.
33    ///
34    /// Predates `getContractVersion`, so this version is never reported by the
35    /// contract itself - it is only reached through selector probing.
36    pub const V2_GETTERS: Self = Self { major: 1, minor: 7, patch: 3 };
37
38    /// First version exposing keyed fee schedules, per-account fee tiers,
39    /// builder attribution, the perpetual-existence bitmap - and
40    /// `getContractVersion` itself.
41    pub const BUILDER_CODES: Self = Self { major: 1, minor: 7, patch: 4 };
42
43    /// First version whose stored fee-schedule rates are in millionths (ppm)
44    /// rather than hundred-thousandths, and whose fills charge the schedule fee
45    /// on EVERY position size change rather than on additions only.
46    ///
47    /// The two arrived in the same release and neither has a signal of its own,
48    /// so they share the threshold.
49    pub const PPM_FEE_UNIT: Self = Self { major: 1, minor: 7, patch: 5 };
50
51    pub const fn new(major: u64, minor: u64, patch: u64) -> Self { Self { major, minor, patch } }
52
53    pub const fn major(&self) -> u64 { self.major }
54
55    pub const fn minor(&self) -> u64 { self.minor }
56
57    pub const fn patch(&self) -> u64 { self.patch }
58}
59
60impl std::fmt::Display for ContractVersion {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "v1.{}.{}.{}", self.major, self.minor, self.patch)
63    }
64}
65
66/// Feature set of the deployed exchange smart contract.
67///
68/// Each flag guards a group of selectors/events introduced by a single release,
69/// so the SDK can index and snapshot a contract that has not been upgraded to
70/// the revision the SDK targets ([`crate::state::Exchange::revision`]).
71#[derive(Clone, Copy, Debug)]
72pub struct ContractFeatures {
73    version: Option<ContractVersion>,
74    v2_state_getters: bool,
75    keyed_fee_schedules: bool,
76    builder_attribution: bool,
77    perpetual_discovery: bool,
78    ppm_fee_unit: bool,
79}
80
81impl ContractFeatures {
82    /// Everything the SDK targets, with no version reported. Useful as a
83    /// default for locally deployed contracts built from the SDK's own ABI.
84    pub fn current() -> Self {
85        Self {
86            version: None,
87            v2_state_getters: true,
88            keyed_fee_schedules: true,
89            builder_attribution: true,
90            perpetual_discovery: true,
91            ppm_fee_unit: true,
92        }
93    }
94
95    /// Feature set of a known contract version.
96    pub fn of(version: ContractVersion) -> Self {
97        let builder_codes = version >= ContractVersion::BUILDER_CODES;
98        Self {
99            version: Some(version),
100            v2_state_getters: version >= ContractVersion::V2_GETTERS,
101            keyed_fee_schedules: builder_codes,
102            builder_attribution: builder_codes,
103            perpetual_discovery: builder_codes,
104            ppm_fee_unit: version >= ContractVersion::PPM_FEE_UNIT,
105        }
106    }
107
108    /// Version reported by the contract, if it exposes `getContractVersion`
109    /// (v1.1.7.4+).
110    pub fn version(&self) -> Option<ContractVersion> { self.version }
111
112    /// `getPerpetualInfoV2` / `getPositionV2` and the V2 position events
113    /// (`fundingSumScalingExp`, `priceResiduePNSQ16`) are available.
114    pub fn v2_state_getters(&self) -> bool { self.v2_state_getters }
115
116    /// Keyed 8-tier fee schedules with per-account fee tiers are available
117    /// (`getPerpFeeSchedule`, `getFeeScheduleById`, `getAccountFeeTier` and
118    /// the `FeeScheduleSet` / `DefaultPerpFeeScheduleSet` /
119    /// `DefaultRwaFeeScheduleSet` / `PerpFeeSchedIdSet` /
120    /// `AccountFeeTierSet` events).
121    pub fn keyed_fee_schedules(&self) -> bool { self.keyed_fee_schedules }
122
123    /// Builder attribution is available (`execOrderV2` and friends,
124    /// `getOrderV2` and the `OrderRequestV2` / `MakerOrderFilledV2` /
125    /// `TakerOrderFilledV2` events).
126    pub fn builder_attribution(&self) -> bool { self.builder_attribution }
127
128    /// The perpetual-existence bitmap is available
129    /// (`getPerpetualExistsBitmap`), so the set of listed perpetuals can be
130    /// discovered on-chain instead of being configured.
131    pub fn perpetual_discovery(&self) -> bool { self.perpetual_discovery }
132
133    /// Stored fee-schedule rates are in millionths (ppm) rather than
134    /// hundred-thousandths, and every position size change is charged the
135    /// schedule fee - a close or decrease on the removed notional, netted from
136    /// the exit proceeds, where earlier releases charged additions only.
137    ///
138    /// The per-order builder fee is NOT affected: it stays `Per100K` on the
139    /// wire, in order storage and in every event at any version.
140    pub fn ppm_fee_unit(&self) -> bool { self.ppm_fee_unit }
141
142    /// Converter for the fee-SCHEDULE rates this deployment reports, resolving
143    /// the v1.1.7.5 redenomination.
144    ///
145    /// An unknown version reads as the pre-upgrade unit, which is right for
146    /// every contract old enough not to report one.
147    pub fn fee_rate_converter(&self) -> num::Converter {
148        if self.ppm_fee_unit { num::ppm_fee_converter() } else { num::fee_converter() }
149    }
150
151    /// Detects the feature set of the deployed contract at `block_id`.
152    ///
153    /// Reads `getContractVersion()`, which is authoritative on v1.1.7.4+ and
154    /// absent before it - so a revert *proves* the contract predates every
155    /// feature that release introduced. What a revert leaves open is whether
156    /// the deployment is v1.1.7.3b or older, resolved by probing
157    /// `getPerpetualInfoV2` against `probe_perpetual`; unlike `getPositionV2`,
158    /// the perpetual getter does not validate account existence, so the probe
159    /// distinguishes selector presence from state. With no perpetual to probe
160    /// against the V2 getters are assumed present - see
161    /// [`Self::probe_v2_state_getters`] to resolve that once one is known.
162    pub(crate) async fn probe<P: Provider>(
163        instance: &dex::Exchange::ExchangeInstance<P>,
164        block_id: BlockId,
165        probe_perpetual: Option<types::PerpetualId>,
166    ) -> Self {
167        if let Ok(v) = instance
168            .getContractVersion()
169            .block(block_id)
170            .call()
171            .await
172            .map(|v| ContractVersion::new(v.major.to(), v.minor.to(), v.patch.to()))
173        {
174            return Self::of(v);
175        }
176
177        let mut features = Self {
178            version: None,
179            v2_state_getters: true,
180            keyed_fee_schedules: false,
181            builder_attribution: false,
182            perpetual_discovery: false,
183            ppm_fee_unit: false,
184        };
185        if let Some(perp_id) = probe_perpetual {
186            features
187                .probe_v2_state_getters(instance, block_id, perp_id)
188                .await;
189        }
190        features
191    }
192
193    /// Resolves [`Self::v2_state_getters`] on an unversioned contract by
194    /// probing `getPerpetualInfoV2` against a known perpetual.
195    ///
196    /// A no-op once the contract reports a version, which settles the question
197    /// outright.
198    pub(crate) async fn probe_v2_state_getters<P: Provider>(
199        &mut self,
200        instance: &dex::Exchange::ExchangeInstance<P>,
201        block_id: BlockId,
202        perp_id: types::PerpetualId,
203    ) {
204        if self.version.is_some() {
205            return;
206        }
207        self.v2_state_getters = instance
208            .getPerpetualInfoV2(U256::from(perp_id))
209            .block(block_id)
210            .call()
211            .await
212            .is_ok();
213    }
214
215    /// Folds a version reported by `ContractVersionSet` into the feature set.
216    ///
217    /// The signal is authoritative, so it is followed in both directions: a
218    /// downgrade below a feature's threshold withdraws that feature.
219    pub(crate) fn observe_version(&mut self, version: ContractVersion) {
220        *self = Self::of(version);
221    }
222}
223
224impl Default for ContractFeatures {
225    fn default() -> Self { Self::current() }
226}
227
228impl std::fmt::Display for ContractFeatures {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        match self.version {
231            Some(version) => write!(f, "{version}"),
232            None => write!(
233                f,
234                "unversioned ({})",
235                if self.v2_state_getters { "V2 getters" } else { "V0 getters" },
236            ),
237        }
238    }
239}