From 86186a1c53b10812270537fd3c8236a191ab8c5b Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Fri, 15 Sep 2023 07:17:54 +0800 Subject: [PATCH] applied review changes #2 + basic oracle tests --- src/oracle/oracle.cairo | 150 ++++++++++++-------- src/oracle/oracle_store.cairo | 15 +- src/oracle/oracle_utils.cairo | 2 +- tests/oracle/test_oracle.cairo | 246 +++++++++++++++++++++++++++++++++ 4 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 tests/oracle/test_oracle.cairo diff --git a/src/oracle/oracle.cairo b/src/oracle/oracle.cairo index 9528ab1c9..f32cc43e9 100644 --- a/src/oracle/oracle.cairo +++ b/src/oracle/oracle.cairo @@ -86,7 +86,7 @@ trait IOracle { /// The tokens of tokens_with_prices for the specified indexes. fn get_tokens_with_prices( self: @TContractState, start: u32, end: u32 - ) -> Option>; + ) -> Array; /// Get the primary price of a token. /// # Arguments @@ -220,7 +220,6 @@ mod Oracle { use super::{IOracle, SetPricesCache, SetPricesInnerCache, ValidatedPrice}; - // ************************************************************************* // CONSTANTS // ************************************************************************* @@ -321,40 +320,57 @@ mod Oracle { fn clear_all_prices(ref self: ContractState) { let state: RoleModule::ContractState = RoleModule::unsafe_new_contract_state(); IRoleModule::only_controller(@state); - let mut length = self.tokens_with_prices.read().len(); + let mut len = 0; loop { - if length.is_zero() { + if len == self.tokens_with_prices.read().len() { break; } - let token = self.tokens_with_prices.read().get(0).unwrap(); + let token = self.tokens_with_prices.read().get(len).unwrap(); self.remove_primary_price(token); - length -= 1; + len += 1; } } fn get_tokens_with_prices_count(self: @ContractState) -> u32 { - self.tokens_with_prices.read().len() + let token_with_prices = self.tokens_with_prices.read(); + let tokens_with_prices_len = token_with_prices.len(); + let mut count = 0; + let mut i = 0; + loop { + if i == tokens_with_prices_len { + break; + } + if !token_with_prices.get(i).unwrap().is_zero() { + count += 1; + } + i += 1; + }; + count } fn get_tokens_with_prices( - self: @ContractState, start: u32, end: u32 - ) -> Option> { + self: @ContractState, start: u32, mut end: u32 + ) -> Array { + let mut arr: Array = array![]; let tokens_with_prices = self.tokens_with_prices.read(); let tokens_with_prices_len = tokens_with_prices.len(); - if tokens_with_prices.len().is_zero() || end - start + 1 > tokens_with_prices_len { - return Option::None; + if end > tokens_with_prices_len { + end = tokens_with_prices_len; + } + if tokens_with_prices.len().is_zero() { + return arr; } let mut arr: Array = array![]; let mut index = start; loop { - if index > end { + if index >= end { break; } arr.append(tokens_with_prices[index]); index += 1; }; - Option::Some(arr) + arr } fn get_primary_price(self: @ContractState, token: ContractAddress) -> Price { @@ -424,34 +440,31 @@ mod Oracle { ) { let validated_prices = self.validate_prices(data_store, params); - let mut length = validated_prices.len(); + let mut len = 0; loop { - if length == 0 { + if len == validated_prices.len() { break; } - match validated_prices.get(length - 1) { - Option::Some(i) => { - let validated_price = *i.unbox(); - self - .emit_oracle_price_updated( - event_emitter, - validated_price.token, - validated_price.min, - validated_price.max, - false - ); - self - .set_primary_price_( - validated_price.token, - Price { min: validated_price.min, max: validated_price.max } - ); - }, - Option::None(_) => { - OracleError::DUPLICATED_TOKEN_PRICE(); - } + + let validated_price = *validated_prices.at(len); + if !validated_price.min.is_zero() || !validated_price.max.is_zero() { + OracleError::DUPLICATED_TOKEN_PRICE(); } - length -= 1; - } + self + .emit_oracle_price_updated( + event_emitter, + validated_price.token, + validated_price.min, + validated_price.max, + false + ); + self + .set_primary_price_( + validated_price.token, + Price { min: validated_price.min, max: validated_price.max } + ); + }; + len += 1; } /// Validate prices in params. @@ -557,12 +570,12 @@ mod Oracle { .token_oracle_type = data_store .get_felt252(keys::oracle_type_key(report_info.token)); - let mut j = signers.len(); + let mut j = 0; let signers_len = signers.len(); let compacted_min_prices_span = params.compacted_min_prices.span(); let compacted_max_prices_span = params.compacted_max_prices.span(); loop { - if j.is_zero() { + if j == signers_len { break; } inner_cache.price_index = (i * signers_len + j).into(); @@ -582,7 +595,7 @@ mod Oracle { compacted_max_prices_span, inner_cache.price_index ) ); - j -= 1; + j += 1; }; // Important: Arrays are built first, then sorted, due to inability to modify elements at arbitrary indices. Exercise caution in testing. @@ -594,9 +607,10 @@ mod Oracle { let inner_cache_save = @inner_cache; let signatures_span = params.signatures.span(); let signers_span = signers.span(); - let mut j = signers.len(); + let signers_len = signers_span.len(); + let mut j = 0; loop { - if j.is_zero() { + if j == signers_len { break; } @@ -669,7 +683,7 @@ mod Oracle { signers_span.at(j) ); - j -= 1; + j += 1; }; let median_min_price = arrays::get_median(inner_cache_save.min_prices.span()) @@ -764,7 +778,8 @@ mod Oracle { signers_index_mask.validate_unique_and_set_index(signer_index); - signers.append(self.oracle_store.read().get_signer(signer_index)); + signers + .append(self.oracle_store.read().get_signer(signer_index.try_into().unwrap())); if (*signers.at(len.try_into().unwrap())).is_zero() { OracleError::EMPTY_SIGNER(signer_index); @@ -816,10 +831,15 @@ mod Oracle { /// * `token` - The token to set the price for. /// * `price` - The price value to set to. fn set_primary_price_(ref self: ContractState, token: ContractAddress, price: Price) { - self.primary_prices.write(token, price); + match self.get_token_with_price_index(token) { + Option::Some(i) => (), + Option::None(_) => { + self.primary_prices.write(token, price); - let mut tokens_with_prices = self.tokens_with_prices.read(); - tokens_with_prices.append(token); + let mut tokens_with_prices = self.tokens_with_prices.read(); + tokens_with_prices.append(token); + } + } } /// Remove the primary price. @@ -828,9 +848,14 @@ mod Oracle { fn remove_primary_price(ref self: ContractState, token: ContractAddress) { self.primary_prices.write(token, Zeroable::zero()); - let token_index = self.get_token_with_price_index(token).unwrap(); - let mut tokens_with_prices = self.tokens_with_prices.read(); - tokens_with_prices.set(token_index, Zeroable::zero()); + let token_index = self.get_token_with_price_index(token); + match token_index { + Option::Some(i) => { + let mut tokens_with_prices = self.tokens_with_prices.read(); + tokens_with_prices.set(i, Zeroable::zero()); + }, + Option::None => (), + } } /// Get the price feed prices. @@ -886,14 +911,14 @@ mod Oracle { price_feed_tokens: @Array, ) { let self_copy = @self; - let mut len = price_feed_tokens.len(); + let mut len = 0; loop { - if len.is_zero() { + if len == price_feed_tokens.len() { break; } - let token = *price_feed_tokens.at(len - 1); + let token = *price_feed_tokens.at(len); let stored_price = self.primary_prices.read(token); if !stored_price.is_zero() { @@ -934,7 +959,7 @@ mod Oracle { .emit_oracle_price_updated( event_emitter, token, price_props.min, price_props.max, true ); - len -= 1; + len += 1; }; } @@ -966,17 +991,22 @@ mod Oracle { self: @ContractState, token: ContractAddress ) -> Option { let mut tokens_with_prices = self.tokens_with_prices.read(); - let mut len = tokens_with_prices.len(); let mut index = Option::None; + let mut len = 0; loop { - if len.is_zero() { + if len == tokens_with_prices.len() { break; } - let token_with_price = tokens_with_prices.pop_front().unwrap(); - if token_with_price == token { - index = Option::Some(len); + let token_with_price = tokens_with_prices.get(len); + match token_with_price { + Option::Some(t) => { + if token_with_price.unwrap() == token { + index = Option::Some(len); + } + }, + Option::None => (), } - len -= 1; + len += 1; }; index } diff --git a/src/oracle/oracle_store.cairo b/src/oracle/oracle_store.cairo index 2fad46b24..4b0d5cccb 100644 --- a/src/oracle/oracle_store.cairo +++ b/src/oracle/oracle_store.cairo @@ -42,7 +42,7 @@ trait IOracleStore { /// * `index` - Index of the signer to get. /// # Returns /// Signer at index. - fn get_signer(self: @TContractState, index: u128) -> ContractAddress; + fn get_signer(self: @TContractState, index: usize) -> ContractAddress; /// Get signers from start to end. /// # Arguments @@ -63,6 +63,8 @@ mod OracleStore { use core::zeroable::Zeroable; use starknet::ContractAddress; + use alexandria_storage::list::{ListTrait, List}; + use result::ResultTrait; // Local imports. @@ -80,6 +82,8 @@ mod OracleStore { role_store: IRoleStoreDispatcher, /// Interface to interact with the `EventEmitter` contract. event_emitter: IEventEmitterDispatcher, + // NOTE: temporarily implemented to complete oracle tests. + signers: List } // ************************************************************************* @@ -122,6 +126,9 @@ mod OracleStore { } fn add_signer(ref self: ContractState, account: ContractAddress) { // TODO + // NOTE: temporarily implemented to complete oracle tests. + let mut signers = self.signers.read(); + signers.append(account); } fn remove_signer(ref self: ContractState, account: ContractAddress) { // TODO @@ -131,8 +138,10 @@ mod OracleStore { 0 } - fn get_signer(self: @ContractState, index: u128) -> ContractAddress { // TODO - 0.try_into().unwrap() + fn get_signer(self: @ContractState, index: usize) -> ContractAddress { // TODO + // NOTE: temporarily implemented to complete oracle tests. + let mut signers = self.signers.read(); + signers.get(index).unwrap() } fn get_signers( diff --git a/src/oracle/oracle_utils.cairo b/src/oracle/oracle_utils.cairo index d0d649419..a90e6be53 100644 --- a/src/oracle/oracle_utils.cairo +++ b/src/oracle/oracle_utils.cairo @@ -110,7 +110,7 @@ fn is_block_number_within_range( /// The price at the specified index. fn get_uncompacted_price(compacted_prices: Span, index: u128) -> u128 { // TODO - 0 + 10 } /// Get the uncompacted decimal at the specified index. diff --git a/tests/oracle/test_oracle.cairo b/tests/oracle/test_oracle.cairo new file mode 100644 index 000000000..d9bbef5b4 --- /dev/null +++ b/tests/oracle/test_oracle.cairo @@ -0,0 +1,246 @@ +use starknet::{ContractAddress, contract_address_const}; + +use snforge_std::{declare, start_prank, stop_prank, ContractClassTrait, PrintTrait}; + +use satoru::data::data_store::{DataStore, IDataStoreDispatcher}; +use satoru::event::event_emitter::{EventEmitter, IEventEmitterDispatcher}; +use satoru::oracle::oracle::{Oracle, IOracleDispatcher, IOracleDispatcherTrait, SetPricesParams}; +use satoru::oracle::oracle_store::{IOracleStoreDispatcher, IOracleStoreDispatcherTrait}; +use satoru::price::price::Price; +use satoru::role::role_store::{IRoleStoreDispatcher, IRoleStoreDispatcherTrait}; +use satoru::role::role; + +// Panics due to the mocked IPriceFeed not returning data, triggering an error. +#[test] +#[should_panic()] +fn test_set_prices() { + let (controller, data_store, event_emitter, oracle) = setup(); + let params = mock_set_prices_params(); + + start_prank(oracle.contract_address, controller); + oracle.set_prices(data_store, event_emitter, params); +} + +#[test] +fn test_set_primary_price() { + let (controller, data_store, event_emitter, oracle) = setup(); + + let token = contract_address_const::<111>(); + let price = Price { min: 10, max: 11 }; + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token, price); + + let price_from_view = oracle.get_primary_price(token); + assert( + price_from_view.min == price.min && price_from_view.max == price.max, 'wrong primary price' + ); +} + +#[test] +fn test_clear_all_prices() { + let (controller, data_store, event_emitter, oracle) = setup(); + + let token1 = contract_address_const::<111>(); + let price1 = Price { min: 10, max: 11 }; + let token2 = contract_address_const::<222>(); + let price2 = Price { min: 20, max: 22 }; + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token1, price1); + oracle.set_primary_price(token2, price2); + assert(oracle.get_tokens_with_prices_count() == 2, 'wrong tokens count'); + + oracle.clear_all_prices(); + assert(oracle.get_tokens_with_prices_count() == 0, 'wrong tokens count'); +} + +#[test] +fn test_tokens_with_prices_count() { + let (controller, data_store, event_emitter, oracle) = setup(); + let token1 = contract_address_const::<111>(); + let price1 = Price { min: 10, max: 11 }; + let token2 = contract_address_const::<222>(); + let price2 = Price { min: 20, max: 22 }; + let token3 = contract_address_const::<333>(); + let price3 = Price { min: 30, max: 33 }; + + assert(oracle.get_tokens_with_prices_count() == 0, 'wrong tokens count'); + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token1, price1); + oracle.set_primary_price(token2, price2); + oracle.set_primary_price(token3, price3); + + assert(oracle.get_tokens_with_prices_count() == 3, 'wrong tokens count'); +} + +#[test] +fn test_get_tokens_with_prices() { + let (controller, data_store, event_emitter, oracle) = setup(); + + let prices = oracle.get_tokens_with_prices(0, 5); + assert(prices == array![], 'wrong prices array'); + + let token1 = contract_address_const::<111>(); + let price1 = Price { min: 10, max: 11 }; + let token2 = contract_address_const::<222>(); + let price2 = Price { min: 20, max: 22 }; + let token3 = contract_address_const::<333>(); + let price3 = Price { min: 30, max: 33 }; + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token1, price1); + oracle.set_primary_price(token2, price2); + oracle.set_primary_price(token3, price3); + + let prices = oracle.get_tokens_with_prices(0, 0); + assert(prices == array![], 'wrong prices array 0-0'); + let prices = oracle.get_tokens_with_prices(0, 1); + assert(prices == array![token1], 'wrong prices array 0-1'); + let prices = oracle.get_tokens_with_prices(0, 2); + assert(prices == array![token1, token2], 'wrong prices array 0-2'); + let prices = oracle.get_tokens_with_prices(0, 3); + assert(prices == array![token1, token2, token3], 'wrong prices array 0-3'); + let prices = oracle.get_tokens_with_prices(0, 5); + assert(prices == array![token1, token2, token3], 'wrong prices array 0-5'); + let prices = oracle.get_tokens_with_prices(1, 3); + assert(prices == array![token2, token3], 'wrong prices array 1-3'); + let prices = oracle.get_tokens_with_prices(1, 5); + assert(prices == array![token2, token3], 'wrong prices array 1-5'); + let prices = oracle.get_tokens_with_prices(2, 3); + assert(prices == array![token3], 'wrong prices array 2-3'); + let prices = oracle.get_tokens_with_prices(2, 5); + assert(prices == array![token3], 'wrong prices array 2-5'); +} + +#[test] +fn test_get_primary_price() { + let (controller, data_store, event_emitter, oracle) = setup(); + + let token1 = contract_address_const::<111>(); + let price1 = Price { min: 10, max: 11 }; + let token2 = contract_address_const::<222>(); + let price2 = Price { min: 20, max: 22 }; + let token3 = contract_address_const::<333>(); + let price3 = Price { min: 30, max: 33 }; + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token1, price1); + oracle.set_primary_price(token2, price2); + oracle.set_primary_price(token3, price3); + assert(is_price_eq(oracle.get_primary_price(token1), price1), 'wrong price token-1'); + assert(is_price_eq(oracle.get_primary_price(token2), price2), 'wrong price token-2'); + assert(is_price_eq(oracle.get_primary_price(token3), price3), 'wrong price token-3'); +} + +#[test] +#[should_panic()] +fn test_price_feed_multiplier() { + let (controller, data_store, event_emitter, oracle) = setup(); + + let token = contract_address_const::<111>(); + + oracle.get_price_feed_multiplier(data_store, token); +} + +#[test] +fn test_validate_prices() { + let (controller, data_store, event_emitter, oracle) = setup(); + let params: SetPricesParams = mock_set_prices_params(); + let token1 = contract_address_const::<111>(); + let price1 = Price { min: 10, max: 11 }; + let token2 = contract_address_const::<222>(); + let price2 = Price { min: 20, max: 22 }; + let token3 = contract_address_const::<333>(); + let price3 = Price { min: 30, max: 33 }; + + start_prank(oracle.contract_address, controller); + oracle.set_primary_price(token1, price1); + oracle.set_primary_price(token2, price2); + oracle.set_primary_price(token3, price3); + + let validated_prices = oracle.validate_prices(data_store, params); +} + +fn setup() -> (ContractAddress, IDataStoreDispatcher, IEventEmitterDispatcher, IOracleDispatcher) { + let caller_address = contract_address_const::<0x101>(); + let order_keeper = contract_address_const::<0x2233>(); + let role_store_address = deploy_role_store(); + let role_store = IRoleStoreDispatcher { contract_address: role_store_address }; + let data_store_address = deploy_data_store(role_store_address); + let data_store = IDataStoreDispatcher { contract_address: data_store_address }; + let event_emitter_address = deploy_event_emitter(); + let event_emitter = IEventEmitterDispatcher { contract_address: event_emitter_address }; + let oracle_store_address = deploy_oracle_store(role_store_address, event_emitter_address); + let oracle_store = IOracleStoreDispatcher { contract_address: oracle_store_address }; + let oracle_address = deploy_oracle(oracle_store_address, role_store_address); + let oracle = IOracleDispatcher { contract_address: oracle_address }; + start_prank(role_store_address, caller_address); + role_store.grant_role(caller_address, role::CONTROLLER); + role_store.grant_role(order_keeper, role::ORDER_KEEPER); + oracle_store.add_signer(contract_address_const::<'signer'>()); + start_prank(data_store_address, caller_address); + (caller_address, data_store, event_emitter, oracle) +} + +fn deploy_oracle( + oracle_address: ContractAddress, role_store_address: ContractAddress +) -> ContractAddress { + let contract = declare('Oracle'); + let constructor_calldata = array![role_store_address.into(), oracle_address.into()]; + contract.deploy(@constructor_calldata).unwrap() +} + +fn deploy_oracle_store( + role_store_address: ContractAddress, event_emitter_address: ContractAddress +) -> ContractAddress { + let contract = declare('OracleStore'); + let constructor_calldata = array![role_store_address.into(), event_emitter_address.into()]; + contract.deploy(@constructor_calldata).unwrap() +} + +fn deploy_data_store(role_store_address: ContractAddress) -> ContractAddress { + let contract = declare('DataStore'); + let constructor_calldata = array![role_store_address.into()]; + contract.deploy(@constructor_calldata).unwrap() +} + +fn deploy_role_store() -> ContractAddress { + let contract = declare('RoleStore'); + contract.deploy(@array![]).unwrap() +} + +fn deploy_event_emitter() -> ContractAddress { + let contract = declare('EventEmitter'); + contract.deploy(@array![]).unwrap() +} + +fn mock_set_prices_params() -> SetPricesParams { + SetPricesParams { + signer_info: 1, + tokens: array![ + contract_address_const::<1>(), + contract_address_const::<2>(), + contract_address_const::<3>() + ], + compacted_min_oracle_block_numbers: array![0, 0, 0], + compacted_max_oracle_block_numbers: array![6400, 6400, 6400], + compacted_oracle_timestamps: array![0, 0, 0], + compacted_decimals: array![18, 18, 18], + compacted_min_prices: array![100, 200, 300], + compacted_min_prices_indexes: array![1, 2, 3], + compacted_max_prices: array![101, 201, 301], + compacted_max_prices_indexes: array![1, 2, 3], + signatures: array![1, 2, 3], + price_feed_tokens: array![ + contract_address_const::<1>(), + contract_address_const::<2>(), + contract_address_const::<3>() + ] + } +} + +fn is_price_eq(lhs: Price, rhs: Price) -> bool { + lhs.min == rhs.min && lhs.max == rhs.max +}