multiversx_price_aggregator_sc/
lib.rs1#![no_std]
2#![allow(unknown_lints)]
4#![allow(clippy::collapsible_if)]
5#![allow(clippy::manual_is_multiple_of)]
6
7multiversx_sc::imports!();
8
9mod events;
10pub mod median;
11pub mod price_aggregator_data;
12
13use events::{Round, Timestamp};
14use multiversx_sc_modules::staking;
15use price_aggregator_data::{OracleStatus, PriceFeed, TimestampedPrice, TokenPair};
16
17const SUBMISSION_LIST_MAX_LEN: usize = 50;
18const SUBMISSION_LIST_MIN_LEN: usize = 3;
19const FIRST_SUBMISSION_TIMESTAMP_MAX_DIFF_SECONDS: DurationSeconds = DurationSeconds::new(30);
20pub const MAX_ROUND_DURATION_SECONDS: DurationSeconds = DurationSeconds::new(1_800); const PAUSED_ERROR_MSG: &[u8] = b"Contract is paused";
22const PAIR_DECIMALS_NOT_CONFIGURED_ERROR: &[u8] = b"pair decimals not configured";
23const WRONG_NUMBER_OF_DECIMALS_ERROR: &[u8] = b"wrong number of decimals";
24
25#[multiversx_sc::contract]
26pub trait PriceAggregator:
27 multiversx_sc_modules::pause::PauseModule + staking::StakingModule + events::EventsModule
28{
29 #[init]
30 fn init(
31 &self,
32 staking_token: EgldOrEsdtTokenIdentifier,
33 staking_amount: BigUint,
34 slash_amount: BigUint,
35 slash_quorum: usize,
36 submission_count: usize,
37 oracles: MultiValueEncoded<ManagedAddress>,
38 ) {
39 self.init_staking_module(
40 &staking_token,
41 &staking_amount,
42 &slash_amount,
43 slash_quorum,
44 &oracles.to_vec(),
45 );
46
47 self.add_oracles(oracles);
48
49 self.require_valid_submission_count(submission_count);
50 self.submission_count().set(submission_count);
51
52 self.set_paused(true);
53 }
54
55 #[upgrade]
56 fn upgrade(&self) {
57 self.set_paused(true);
58 }
59
60 #[only_owner]
61 #[endpoint(changeAmounts)]
62 fn change_amounts(&self, staking_amount: BigUint, slash_amount: BigUint) {
63 require!(
64 staking_amount > 0 && slash_amount > 0,
65 "Staking and slash amount cannot be 0"
66 );
67 require!(
68 slash_amount <= staking_amount,
69 "Slash amount cannot be higher than required stake"
70 );
71
72 let user_whitelist = self.user_whitelist();
73 let slash_quorum = self.slash_quorum().get();
74
75 let mut users_owning_new_amount = 0;
76 for user in user_whitelist.iter() {
77 if staking_amount < self.staked_amount(&user).get() {
78 users_owning_new_amount += 1;
79 }
80 if users_owning_new_amount > slash_quorum {
81 break;
82 }
83 }
84
85 require!(
86 users_owning_new_amount > slash_quorum,
87 "New staking amount is too big compared to members staked amount"
88 );
89 self.required_stake_amount().set(staking_amount);
90 self.slash_amount().set(slash_amount);
91 }
92
93 #[only_owner]
94 #[endpoint(addOracles)]
95 fn add_oracles(&self, oracles: MultiValueEncoded<ManagedAddress>) {
96 let mut oracle_mapper = self.oracle_status();
97 for oracle in oracles {
98 if !oracle_mapper.contains_key(&oracle) {
99 let _ = oracle_mapper.insert(
100 oracle.clone(),
101 OracleStatus {
102 total_submissions: 0,
103 accepted_submissions: 0,
104 },
105 );
106 self.add_board_member(oracle);
107 }
108 }
109 }
110
111 #[only_owner]
114 #[endpoint(removeOracles)]
115 fn remove_oracles(&self, submission_count: usize, oracles: MultiValueEncoded<ManagedAddress>) {
116 let mut oracle_mapper = self.oracle_status();
117 for oracle in oracles {
118 let _ = oracle_mapper.remove(&oracle);
119 self.remove_board_member(&oracle);
120 }
121
122 self.require_valid_submission_count(submission_count);
123 self.submission_count().set(submission_count);
124 }
125
126 #[endpoint]
127 fn submit(
128 &self,
129 from: ManagedBuffer,
130 to: ManagedBuffer,
131 submission_timestamp: Timestamp,
132 price: BigUint,
133 decimals: u8,
134 ) {
135 self.require_not_paused();
136 self.require_is_oracle();
137
138 self.require_valid_submission_timestamp(submission_timestamp);
139
140 self.check_decimals(&from, &to, decimals);
141
142 self.submit_unchecked(from, to, price, decimals);
143 }
144
145 fn submit_unchecked(
146 &self,
147 from: ManagedBuffer,
148 to: ManagedBuffer,
149 price: BigUint,
150 decimals: u8,
151 ) {
152 let token_pair = TokenPair { from, to };
153 let mut submissions = self
154 .submissions()
155 .entry(token_pair.clone())
156 .or_default()
157 .get();
158
159 let first_sub_time_mapper = self.first_submission_timestamp(&token_pair);
160 let last_sub_time_mapper = self.last_submission_timestamp(&token_pair);
161
162 let mut round_id = 0;
163 let wrapped_rounds = self.rounds().get(&token_pair);
164 if wrapped_rounds.is_some() {
165 round_id = wrapped_rounds.unwrap().len() + 1;
166 }
167
168 let current_timestamp = self.blockchain().get_block_timestamp_seconds();
169 let mut is_first_submission = false;
170 let mut first_submission_timestamp = if submissions.is_empty() {
171 first_sub_time_mapper.set(current_timestamp);
172 is_first_submission = true;
173
174 current_timestamp
175 } else {
176 first_sub_time_mapper.get()
177 };
178
179 if current_timestamp > first_submission_timestamp + MAX_ROUND_DURATION_SECONDS {
181 submissions.clear();
182 first_sub_time_mapper.set(current_timestamp);
183 last_sub_time_mapper.set(current_timestamp);
184
185 first_submission_timestamp = current_timestamp;
186 is_first_submission = true;
187 self.discard_round_event(&token_pair.from.clone(), &token_pair.to.clone(), round_id)
188 }
189
190 let caller = self.blockchain().get_caller();
191 let has_caller_already_submitted = submissions.contains_key(&caller);
192 let accepted = !has_caller_already_submitted
193 && (is_first_submission || current_timestamp >= first_submission_timestamp);
194 if accepted {
195 submissions.insert(caller.clone(), price.clone());
196 last_sub_time_mapper.set(current_timestamp);
197
198 self.create_new_round(token_pair.clone(), round_id, submissions, decimals);
199 self.add_submission_event(
200 &token_pair.from.clone(),
201 &token_pair.to.clone(),
202 round_id,
203 &price,
204 );
205 } else {
206 self.emit_discard_submission_event(
207 &token_pair,
208 round_id,
209 current_timestamp,
210 first_submission_timestamp,
211 has_caller_already_submitted,
212 );
213 }
214
215 self.oracle_status()
216 .entry(self.blockchain().get_caller())
217 .and_modify(|oracle_status| {
218 oracle_status.accepted_submissions += accepted as u64;
219 oracle_status.total_submissions += 1;
220 });
221 }
222
223 fn require_valid_submission_timestamp(&self, submission_timestamp: Timestamp) {
224 let current_timestamp = self.blockchain().get_block_timestamp_seconds();
225 require!(
226 submission_timestamp <= current_timestamp,
227 "Timestamp is from the future"
228 );
229 require!(
230 current_timestamp - submission_timestamp <= FIRST_SUBMISSION_TIMESTAMP_MAX_DIFF_SECONDS,
231 "First submission too old"
232 );
233 }
234
235 #[endpoint(submitBatch)]
236 fn submit_batch(
237 &self,
238 submissions: MultiValueEncoded<
239 MultiValue5<ManagedBuffer, ManagedBuffer, Timestamp, BigUint, u8>,
240 >,
241 ) {
242 self.require_not_paused();
243 self.require_is_oracle();
244
245 for (from, to, submission_timestamp, price, decimals) in submissions
246 .into_iter()
247 .map(|submission| submission.into_tuple())
248 {
249 self.require_valid_submission_timestamp(submission_timestamp);
250
251 self.check_decimals(&from, &to, decimals);
252
253 self.submit_unchecked(from, to, price, decimals);
254 }
255 }
256
257 fn require_is_oracle(&self) {
258 let caller = self.blockchain().get_caller();
259 require!(
260 self.oracle_status().contains_key(&caller) && self.is_staked_board_member(&caller),
261 "only oracles allowed"
262 );
263 }
264
265 fn require_valid_submission_count(&self, submission_count: usize) {
266 require!(
267 submission_count >= SUBMISSION_LIST_MIN_LEN
268 && submission_count <= self.oracle_status().len()
269 && submission_count <= SUBMISSION_LIST_MAX_LEN,
270 "Invalid submission count"
271 )
272 }
273
274 fn create_new_round(
275 &self,
276 token_pair: TokenPair<Self::Api>,
277 round: Round,
278 mut submissions: MapMapper<ManagedAddress, BigUint>,
279 decimals: u8,
280 ) {
281 let submissions_len = submissions.len();
282 if submissions_len >= self.submission_count().get() {
283 require!(
284 submissions_len <= SUBMISSION_LIST_MAX_LEN,
285 "submission list capacity exceeded"
286 );
287
288 let mut submissions_vec = ArrayVec::<BigUint, SUBMISSION_LIST_MAX_LEN>::new();
289 for submission_value in submissions.values() {
290 submissions_vec.push(submission_value);
291 }
292
293 let price_result = median::calculate(submissions_vec.as_mut_slice());
294 let price_opt = price_result.unwrap_or_else(|err| sc_panic!(err.as_bytes()));
295 let price = price_opt.unwrap_or_else(|| sc_panic!("no submissions"));
296 let price_feed = TimestampedPrice {
297 price,
298 timestamp: self.blockchain().get_block_timestamp_seconds(),
299 decimals,
300 };
301
302 submissions.clear();
303 self.first_submission_timestamp(&token_pair).clear();
304 self.last_submission_timestamp(&token_pair).clear();
305
306 self.rounds()
307 .entry(token_pair.clone())
308 .or_default()
309 .get()
310 .push(&price_feed);
311 self.emit_new_round_event(&token_pair, round, &price_feed);
312 }
313 }
314
315 #[view(latestRoundData)]
316 fn latest_round_data(&self) -> MultiValueEncoded<PriceFeed<Self::Api>> {
317 self.require_not_paused();
318 require!(!self.rounds().is_empty(), "no completed rounds");
319
320 let mut result = MultiValueEncoded::new();
321 for (token_pair, round_values) in self.rounds().iter() {
322 result.push(self.make_price_feed(token_pair, round_values));
323 }
324
325 result
326 }
327
328 #[view(latestPriceFeed)]
329 fn latest_price_feed(
330 &self,
331 from: ManagedBuffer,
332 to: ManagedBuffer,
333 ) -> MultiValue6<u32, ManagedBuffer, ManagedBuffer, Timestamp, BigUint, u8> {
334 require!(self.not_paused(), PAUSED_ERROR_MSG);
335
336 let token_pair = TokenPair { from, to };
337 let round_values = self
338 .rounds()
339 .get(&token_pair)
340 .unwrap_or_else(|| sc_panic!("token pair not found"));
341 let feed = self.make_price_feed(token_pair, round_values);
342 (
343 feed.round_id,
344 feed.from,
345 feed.to,
346 feed.timestamp,
347 feed.price,
348 feed.decimals,
349 )
350 .into()
351 }
352
353 #[view(latestPriceFeedOptional)]
354 fn latest_price_feed_optional(
355 &self,
356 from: ManagedBuffer,
357 to: ManagedBuffer,
358 ) -> OptionalValue<MultiValue6<u32, ManagedBuffer, ManagedBuffer, Timestamp, BigUint, u8>> {
359 Some(self.latest_price_feed(from, to)).into()
360 }
361
362 #[only_owner]
363 #[endpoint(setSubmissionCount)]
364 fn set_submission_count(&self, submission_count: usize) {
365 self.require_valid_submission_count(submission_count);
366 self.submission_count().set(submission_count);
367 }
368
369 fn make_price_feed(
370 &self,
371 token_pair: TokenPair<Self::Api>,
372 round_values: VecMapper<TimestampedPrice<Self::Api>>,
373 ) -> PriceFeed<Self::Api> {
374 let round_id = round_values.len();
375 let last_price = round_values.get(round_id);
376
377 PriceFeed {
378 round_id: round_id as u32,
379 from: token_pair.from,
380 to: token_pair.to,
381 timestamp: last_price.timestamp,
382 price: last_price.price,
383 decimals: last_price.decimals,
384 }
385 }
386
387 #[view(getOracles)]
388 #[title("oracles")]
389 fn get_oracles(&self) -> MultiValueEncoded<ManagedAddress> {
390 let mut result = MultiValueEncoded::new();
391 for key in self.oracle_status().keys() {
392 result.push(key);
393 }
394 result
395 }
396
397 fn clear_submissions(&self, token_pair: &TokenPair<Self::Api>) {
398 if let Some(mut pair_submission_mapper) = self.submissions().get(token_pair) {
399 pair_submission_mapper.clear();
400 }
401 self.first_submission_timestamp(token_pair).clear();
402 self.last_submission_timestamp(token_pair).clear();
403 }
404
405 #[only_owner]
406 #[endpoint(setPairDecimals)]
407 fn set_pair_decimals(&self, from: ManagedBuffer, to: ManagedBuffer, decimals: u8) {
408 let pair_decimals_mapper = self.pair_decimals(&from, &to);
409 if !pair_decimals_mapper.is_empty() {
410 self.require_paused();
411 }
412 pair_decimals_mapper.set(Some(decimals));
413 let pair = TokenPair { from, to };
414 self.clear_submissions(&pair);
415 }
416
417 fn check_decimals(&self, from: &ManagedBuffer, to: &ManagedBuffer, decimals: u8) {
418 let configured_decimals = self.get_pair_decimals(from, to);
419 require!(
420 decimals == configured_decimals,
421 WRONG_NUMBER_OF_DECIMALS_ERROR
422 )
423 }
424
425 #[view(getPairDecimals)]
426 fn get_pair_decimals(&self, from: &ManagedBuffer, to: &ManagedBuffer) -> u8 {
427 self.pair_decimals(from, to)
428 .get()
429 .unwrap_or_else(|| sc_panic!(PAIR_DECIMALS_NOT_CONFIGURED_ERROR))
430 }
431
432 #[storage_mapper("pair_decimals")]
433 fn pair_decimals(
434 &self,
435 from: &ManagedBuffer,
436 to: &ManagedBuffer,
437 ) -> SingleValueMapper<Option<u8>>;
438
439 #[view]
440 #[storage_mapper("submission_count")]
441 fn submission_count(&self) -> SingleValueMapper<usize>;
442
443 #[storage_mapper("oracle_status")]
444 fn oracle_status(&self) -> MapMapper<ManagedAddress, OracleStatus>;
445
446 #[storage_mapper("rounds")]
447 fn rounds(
448 &self,
449 ) -> MapStorageMapper<TokenPair<Self::Api>, VecMapper<TimestampedPrice<Self::Api>>>;
450
451 #[storage_mapper("first_submission_timestamp")]
452 fn first_submission_timestamp(
453 &self,
454 token_pair: &TokenPair<Self::Api>,
455 ) -> SingleValueMapper<Timestamp>;
456
457 #[storage_mapper("last_submission_timestamp")]
458 fn last_submission_timestamp(
459 &self,
460 token_pair: &TokenPair<Self::Api>,
461 ) -> SingleValueMapper<Timestamp>;
462
463 #[storage_mapper("submissions")]
464 fn submissions(
465 &self,
466 ) -> MapStorageMapper<TokenPair<Self::Api>, MapMapper<ManagedAddress, BigUint>>;
467}