1#[cfg(feature = "wasm")]
7use equanetwork_macros::wasm_expose;
8use ethnum::U256;
9
10use super::consts::{PER_1M_DENOMINATOR, Q64_ONE};
11use super::error::{
12 CoreError, ARITHMETIC_OVERFLOW, DIVISION_BY_ZERO, INVALID_MARGIN, INVALID_PRICE,
13};
14use super::U128;
15
16#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18pub struct PriceBand {
19 pub lo_q64: u128,
20 pub hi_q64: u128,
21}
22
23#[cfg_attr(feature = "wasm", wasm_expose)]
25pub fn default_price_q64() -> U128 {
26 U128::from(Q64_ONE)
27}
28
29#[cfg_attr(feature = "wasm", wasm_expose)]
34pub fn ratio_to_q64(numerator: U128, denominator: U128) -> Result<U128, CoreError> {
35 let numerator: u128 = numerator.into();
36 let denominator: u128 = denominator.into();
37 if denominator == 0 {
38 return Err(DIVISION_BY_ZERO);
39 }
40 if numerator == 0 {
41 return Err(INVALID_PRICE);
42 }
43 let q: U256 = (U256::from(numerator) << 64) / U256::from(denominator);
44 let q = u128::try_from(q).map_err(|_| ARITHMETIC_OVERFLOW)?;
45 if q == 0 {
46 return Err(INVALID_PRICE);
47 }
48 Ok(U128::from(q))
49}
50
51#[cfg_attr(feature = "wasm", wasm_expose)]
56pub fn combine_prices_q64(price_in_q64: U128, price_out_q64: U128) -> Result<U128, CoreError> {
57 ratio_to_q64(price_in_q64, price_out_q64)
58}
59
60pub fn price_band_with_margin(price_q64: U128, margin_per_1m: u32) -> Result<PriceBand, CoreError> {
62 let price: u128 = price_q64.into();
63 if price == 0 {
64 return Err(INVALID_PRICE);
65 }
66 if margin_per_1m > PER_1M_DENOMINATOR as u32 {
67 return Err(INVALID_MARGIN);
68 }
69 let denom = U256::from(PER_1M_DENOMINATOR as u128);
70 let p = U256::from(price);
71 let lo_f = U256::from(PER_1M_DENOMINATOR.saturating_sub(margin_per_1m as u64) as u128);
72 let hi_f = U256::from(PER_1M_DENOMINATOR.saturating_add(margin_per_1m as u64) as u128);
73 let lo = u128::try_from((p * lo_f) / denom).map_err(|_| ARITHMETIC_OVERFLOW)?;
74 let hi = u128::try_from((p * hi_f) / denom).map_err(|_| ARITHMETIC_OVERFLOW)?;
75 if lo == 0 {
76 return Err(INVALID_PRICE);
77 }
78 Ok(PriceBand {
79 lo_q64: lo,
80 hi_q64: hi,
81 })
82}
83
84pub fn combine_price_band(
86 in_lo_q64: U128,
87 in_hi_q64: U128,
88 out_lo_q64: U128,
89 out_hi_q64: U128,
90) -> Result<PriceBand, CoreError> {
91 let lo: u128 = combine_prices_q64(in_lo_q64, out_hi_q64)?.into();
92 let hi: u128 = combine_prices_q64(in_hi_q64, out_lo_q64)?.into();
93 if lo == 0 || hi < lo {
94 return Err(INVALID_PRICE);
95 }
96 Ok(PriceBand {
97 lo_q64: lo,
98 hi_q64: hi,
99 })
100}
101
102pub fn combine_feed_band(
104 in_mid_q64: U128,
105 in_conf_q64: U128,
106 out_mid_q64: U128,
107 out_conf_q64: U128,
108) -> Result<PriceBand, CoreError> {
109 let in_mid: u128 = in_mid_q64.into();
110 let in_conf: u128 = in_conf_q64.into();
111 let out_mid: u128 = out_mid_q64.into();
112 let out_conf: u128 = out_conf_q64.into();
113 let in_lo = in_mid.saturating_sub(in_conf);
114 let in_hi = in_mid.saturating_add(in_conf);
115 let out_lo = out_mid.saturating_sub(out_conf);
116 let out_hi = out_mid.saturating_add(out_conf);
117 if in_lo == 0 || out_lo == 0 {
118 return Err(INVALID_PRICE);
119 }
120 combine_price_band(
121 U128::from(in_lo),
122 U128::from(in_hi),
123 U128::from(out_lo),
124 U128::from(out_hi),
125 )
126}
127
128fn ratio_leq(num_a: u128, den_a: u128, num_b: u128, den_b: u128) -> bool {
130 U256::from(num_a) * U256::from(den_b) <= U256::from(den_a) * U256::from(num_b)
131}
132
133#[allow(clippy::too_many_arguments)]
142#[cfg_attr(feature = "wasm", wasm_expose)]
143pub fn assert_synthetic_oracle_within_execution(
144 source_in_q64: U128,
145 source_out_q64: U128,
146 feed_in_mid_q64: U128,
147 feed_in_conf_q64: U128,
148 feed_out_mid_q64: U128,
149 feed_out_conf_q64: U128,
150 margin_in_per_1m: u32,
151 margin_out_per_1m: u32,
152) -> Result<(), CoreError> {
153 let source_in: u128 = source_in_q64.into();
154 let source_out: u128 = source_out_q64.into();
155 let feed_in_mid: u128 = feed_in_mid_q64.into();
156 let feed_in_conf: u128 = feed_in_conf_q64.into();
157 let feed_out_mid: u128 = feed_out_mid_q64.into();
158 let feed_out_conf: u128 = feed_out_conf_q64.into();
159
160 if source_in == 0 || source_out == 0 {
161 return Err(INVALID_PRICE);
162 }
163 let margin_per_1m = core::cmp::min(margin_in_per_1m, margin_out_per_1m);
164 let source_band = price_band_with_margin(U128::from(source_in), margin_per_1m)?;
165
166 let feed_in_lo = feed_in_mid.saturating_sub(feed_in_conf);
167 let feed_in_hi = feed_in_mid.saturating_add(feed_in_conf);
168 let feed_out_lo = feed_out_mid.saturating_sub(feed_out_conf);
169 let feed_out_hi = feed_out_mid.saturating_add(feed_out_conf);
170 if feed_in_lo == 0 || feed_out_lo == 0 {
171 return Err(INVALID_PRICE);
172 }
173
174 if !ratio_leq(source_band.lo_q64, source_out, feed_in_lo, feed_out_hi)
177 || !ratio_leq(feed_in_hi, feed_out_lo, source_band.hi_q64, source_out)
178 {
179 return Err(INVALID_MARGIN);
180 }
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn combine_prices_cancels_quote_currency() {
190 let exec: u128 = combine_prices_q64(U128::from(Q64_ONE * 2), U128::from(Q64_ONE))
191 .unwrap()
192 .into();
193 let oracle: u128 = combine_prices_q64(U128::from(Q64_ONE * 200), U128::from(Q64_ONE * 100))
194 .unwrap()
195 .into();
196 assert_eq!(exec, oracle);
197 assert_eq!(exec, Q64_ONE * 2);
198 }
199
200 #[test]
201 fn ratio_to_q64_from_integer_amounts() {
202 let price: u128 = ratio_to_q64(U128::from(2u128), U128::from(1u128))
203 .unwrap()
204 .into();
205 assert_eq!(price, Q64_ONE * 2);
206 }
207
208 #[test]
209 fn synthetic_oracle_matches_execution() {
210 assert!(assert_synthetic_oracle_within_execution(
211 U128::from(Q64_ONE),
212 U128::from(Q64_ONE),
213 U128::from(Q64_ONE),
214 U128::from(0u128),
215 U128::from(Q64_ONE),
216 U128::from(0u128),
217 5_000,
218 5_000
219 )
220 .is_ok());
221 let near = Q64_ONE + Q64_ONE / 25;
223 assert!(assert_synthetic_oracle_within_execution(
224 U128::from(Q64_ONE),
225 U128::from(Q64_ONE),
226 U128::from(near),
227 U128::from(0u128),
228 U128::from(Q64_ONE),
229 U128::from(0u128),
230 50_000,
231 50_000
232 )
233 .is_ok());
234 let far = Q64_ONE + Q64_ONE / 10;
236 assert!(assert_synthetic_oracle_within_execution(
237 U128::from(Q64_ONE),
238 U128::from(Q64_ONE),
239 U128::from(far),
240 U128::from(0u128),
241 U128::from(Q64_ONE),
242 U128::from(0u128),
243 50_000,
244 50_000
245 )
246 .is_err());
247 let conf = Q64_ONE / 50;
249 assert!(assert_synthetic_oracle_within_execution(
250 U128::from(Q64_ONE),
251 U128::from(Q64_ONE),
252 U128::from(Q64_ONE),
253 U128::from(conf),
254 U128::from(Q64_ONE),
255 U128::from(0u128),
256 5_000,
257 5_000
258 )
259 .is_err());
260 }
261
262 #[test]
263 fn mixed_quote_currencies_cancel_in_combined_ratio() {
264 let source_in = Q64_ONE * 2;
267 let feed_in = Q64_ONE * 200;
268 let feed_out = Q64_ONE * 100;
269 assert!(assert_synthetic_oracle_within_execution(
270 U128::from(source_in),
271 U128::from(Q64_ONE),
272 U128::from(feed_in),
273 U128::from(0u128),
274 U128::from(feed_out),
275 U128::from(0u128),
276 5_000,
277 5_000
278 )
279 .is_ok());
280 let feed_in_far = Q64_ONE * 220;
282 assert!(assert_synthetic_oracle_within_execution(
283 U128::from(source_in),
284 U128::from(Q64_ONE),
285 U128::from(feed_in_far),
286 U128::from(0u128),
287 U128::from(feed_out),
288 U128::from(0u128),
289 5_000,
290 5_000
291 )
292 .is_err());
293 }
294}