defuse_core/
fees.rs

1use core::{
2    fmt::{self, Display},
3    ops::{Add, Div, Mul, Not, Sub},
4};
5use std::borrow::Cow;
6
7use defuse_num_utils::{CheckedAdd, CheckedMulDiv, CheckedSub};
8use near_sdk::{
9    AccountId, AccountIdRef,
10    borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
11    near,
12};
13use thiserror::Error as ThisError;
14
15#[near(serializers = [borsh, json])]
16#[derive(Debug, Clone)]
17pub struct FeesConfig {
18    pub fee: Pips,
19    pub fee_collector: AccountId,
20}
21
22/// 1 pip == 1/100th of bip == 0.0001%
23#[near(serializers = [json])]
24#[serde(try_from = "u32")]
25#[derive(
26    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, BorshSerialize, BorshSchema,
27)]
28#[borsh(crate = "::near_sdk::borsh")]
29pub struct Pips(u32);
30
31impl Pips {
32    pub const ZERO: Self = Self(0);
33    pub const ONE_PIP: Self = Self(1);
34    pub const ONE_BIP: Self = Self(Self::ONE_PIP.as_pips() * 100);
35    pub const ONE_PERCENT: Self = Self(Self::ONE_BIP.as_pips() * 100);
36    pub const MAX: Self = Self(Self::ONE_PERCENT.as_pips() * 100);
37
38    #[inline]
39    pub const fn from_pips(pips: u32) -> Option<Self> {
40        if pips > Self::MAX.as_pips() {
41            return None;
42        }
43        Some(Self(pips))
44    }
45
46    #[inline]
47    pub const fn from_bips(bips: u32) -> Option<Self> {
48        Self::ONE_BIP.checked_mul(bips)
49    }
50
51    #[inline]
52    pub const fn from_percent(percent: u32) -> Option<Self> {
53        Self::ONE_PERCENT.checked_mul(percent)
54    }
55
56    #[inline]
57    pub const fn as_pips(self) -> u32 {
58        self.0
59    }
60
61    #[inline]
62    pub const fn as_bips(self) -> u32 {
63        self.as_pips() / Self::ONE_BIP.as_pips()
64    }
65
66    #[inline]
67    pub const fn as_percent(self) -> u32 {
68        self.as_pips() / Self::ONE_PERCENT.as_pips()
69    }
70
71    #[inline]
72    pub const fn is_zero(&self) -> bool {
73        self.0 == 0
74    }
75
76    #[inline]
77    pub fn as_f64(self) -> f64 {
78        f64::from(self.as_pips()) / f64::from(Self::MAX.as_pips())
79    }
80
81    #[inline]
82    pub const fn checked_mul(self, rhs: u32) -> Option<Self> {
83        let Some(pips) = self.as_pips().checked_mul(rhs) else {
84            return None;
85        };
86        Self::from_pips(pips)
87    }
88
89    #[inline]
90    pub const fn checked_div(self, rhs: u32) -> Option<Self> {
91        let Some(pips) = self.as_pips().checked_div(rhs) else {
92            return None;
93        };
94        Some(Self(pips))
95    }
96
97    #[must_use]
98    #[inline]
99    pub const fn invert(self) -> Self {
100        Self(Self::MAX.as_pips() - self.as_pips())
101    }
102
103    #[inline]
104    pub fn fee(self, amount: u128) -> u128 {
105        amount
106            .checked_mul_div(self.as_pips().into(), Self::MAX.as_pips().into())
107            .unwrap_or_else(|| unreachable!())
108    }
109
110    #[inline]
111    pub fn fee_ceil(self, amount: u128) -> u128 {
112        amount
113            .checked_mul_div_ceil(self.as_pips().into(), Self::MAX.as_pips().into())
114            .unwrap_or_else(|| unreachable!())
115    }
116}
117
118impl CheckedAdd for Pips {
119    #[inline]
120    fn checked_add(self, rhs: Self) -> Option<Self> {
121        self.as_pips()
122            .checked_add(rhs.as_pips())
123            .and_then(Self::from_pips)
124    }
125}
126
127impl Add for Pips {
128    type Output = Self;
129
130    #[inline]
131    fn add(self, rhs: Self) -> Self::Output {
132        self.checked_add(rhs).unwrap()
133    }
134}
135
136impl CheckedSub for Pips {
137    #[inline]
138    fn checked_sub(self, rhs: Self) -> Option<Self> {
139        self.as_pips()
140            .checked_sub(rhs.as_pips())
141            .and_then(Self::from_pips)
142    }
143}
144
145impl Sub for Pips {
146    type Output = Self;
147
148    #[inline]
149    fn sub(self, rhs: Self) -> Self::Output {
150        self.checked_sub(rhs).unwrap()
151    }
152}
153
154impl Mul<u32> for Pips {
155    type Output = Self;
156
157    #[inline]
158    fn mul(self, rhs: u32) -> Self::Output {
159        self.checked_mul(rhs).unwrap()
160    }
161}
162
163impl Div<u32> for Pips {
164    type Output = Self;
165
166    #[inline]
167    fn div(self, rhs: u32) -> Self::Output {
168        self.checked_div(rhs).unwrap()
169    }
170}
171
172impl Not for Pips {
173    type Output = Self;
174
175    fn not(self) -> Self::Output {
176        self.invert()
177    }
178}
179
180impl Display for Pips {
181    #[inline]
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{:.4}%", self.as_f64() * 100f64)
184    }
185}
186
187impl TryFrom<u32> for Pips {
188    type Error = PipsOutOfRange;
189
190    #[inline]
191    fn try_from(pips: u32) -> Result<Self, Self::Error> {
192        Self::from_pips(pips).ok_or(PipsOutOfRange)
193    }
194}
195
196#[derive(Debug, ThisError)]
197#[error("out of range: 0..={}", Pips::MAX.as_pips())]
198pub struct PipsOutOfRange;
199
200#[must_use = "make sure to `.emit()` this event"]
201#[near(serializers = [json])]
202#[derive(Debug, Clone)]
203pub struct FeeChangedEvent {
204    pub old_fee: Pips,
205    pub new_fee: Pips,
206}
207
208#[must_use = "make sure to `.emit()` this event"]
209#[near(serializers = [json])]
210#[derive(Debug, Clone)]
211pub struct FeeCollectorChangedEvent<'a> {
212    pub old_fee_collector: Cow<'a, AccountIdRef>,
213    pub new_fee_collector: Cow<'a, AccountIdRef>,
214}
215
216impl BorshDeserialize for Pips {
217    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
218        let pips: u32 = near_sdk::borsh::BorshDeserialize::deserialize_reader(reader)?;
219        Self::from_pips(pips).ok_or_else(|| {
220            std::io::Error::new(
221                std::io::ErrorKind::InvalidData,
222                format!("{PipsOutOfRange} - Invalid pips value: {pips}"),
223            )
224        })
225    }
226}