diff --git a/contracts/core/price-aggregator/tests/price_agg_setup/mod.rs b/contracts/core/price-aggregator/tests/price_agg_setup/mod.rs deleted file mode 100644 index 819baa5fc4..0000000000 --- a/contracts/core/price-aggregator/tests/price_agg_setup/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use multiversx_price_aggregator_sc::{staking::StakingModule, PriceAggregator}; -use multiversx_sc::types::{Address, EgldOrEsdtTokenIdentifier, MultiValueEncoded}; -use multiversx_sc_modules::pause::PauseModule; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_buffer, rust_biguint, - testing_framework::{BlockchainStateWrapper, ContractObjWrapper, TxResult}, - DebugApi, -}; - -pub const NR_ORACLES: usize = 4; -pub const SUBMISSION_COUNT: usize = 3; -pub const DECIMALS: u8 = 0; -pub static EGLD_TICKER: &[u8] = b"EGLD"; -pub static USD_TICKER: &[u8] = b"USDC"; - -pub const STAKE_AMOUNT: u64 = 20; -pub const SLASH_AMOUNT: u64 = 10; -pub const SLASH_QUORUM: usize = 2; - -pub struct PriceAggSetup -where - PriceAggObjBuilder: - 'static + Copy + Fn() -> multiversx_price_aggregator_sc::ContractObj, -{ - pub b_mock: BlockchainStateWrapper, - pub owner: Address, - pub oracles: Vec
, - pub price_agg: ContractObjWrapper< - multiversx_price_aggregator_sc::ContractObj, - PriceAggObjBuilder, - >, -} - -impl PriceAggSetup -where - PriceAggObjBuilder: - 'static + Copy + Fn() -> multiversx_price_aggregator_sc::ContractObj, -{ - pub fn new(builder: PriceAggObjBuilder) -> Self { - let rust_zero = rust_biguint!(0); - let mut b_mock = BlockchainStateWrapper::new(); - let owner = b_mock.create_user_account(&rust_zero); - - let mut oracles = Vec::new(); - for _ in 0..NR_ORACLES { - let oracle = b_mock.create_user_account(&rust_biguint!(STAKE_AMOUNT)); - oracles.push(oracle); - } - - let price_agg = - b_mock.create_sc_account(&rust_zero, Some(&owner), builder, "price_agg_path"); - - let current_timestamp = 100; - b_mock.set_block_timestamp(current_timestamp); - - // init price aggregator - b_mock - .execute_tx(&owner, &price_agg, &rust_zero, |sc| { - let mut oracle_args = MultiValueEncoded::new(); - for oracle in &oracles { - oracle_args.push(managed_address!(oracle)); - } - - sc.init( - EgldOrEsdtTokenIdentifier::egld(), - managed_biguint!(STAKE_AMOUNT), - managed_biguint!(SLASH_AMOUNT), - SLASH_QUORUM, - SUBMISSION_COUNT, - oracle_args, - ); - }) - .assert_ok(); - - for oracle in &oracles { - b_mock - .execute_tx(oracle, &price_agg, &rust_biguint!(STAKE_AMOUNT), |sc| { - sc.stake(); - }) - .assert_ok(); - } - - Self { - b_mock, - oracles, - owner, - price_agg, - } - } - - pub fn set_pair_decimals(&mut self, from: &[u8], to: &[u8], decimals: u8) { - self.b_mock - .execute_tx(&self.owner, &self.price_agg, &rust_biguint!(0), |sc| { - sc.set_pair_decimals(managed_buffer!(from), managed_buffer!(to), decimals); - }) - .assert_ok(); - } - - pub fn unpause(&mut self) { - self.b_mock - .execute_tx(&self.owner, &self.price_agg, &rust_biguint!(0), |sc| { - sc.unpause_endpoint(); - }) - .assert_ok(); - } - - pub fn submit(&mut self, oracle: &Address, timestamp: u64, price: u64) -> TxResult { - self.b_mock - .execute_tx(oracle, &self.price_agg, &rust_biguint!(0), |sc| { - sc.submit( - managed_buffer!(EGLD_TICKER), - managed_buffer!(USD_TICKER), - timestamp, - managed_biguint!(price), - DECIMALS, - ); - }) - } -} diff --git a/contracts/core/price-aggregator/tests/price_agg_tests.rs b/contracts/core/price-aggregator/tests/price_agg_tests.rs deleted file mode 100644 index 3a40450903..0000000000 --- a/contracts/core/price-aggregator/tests/price_agg_tests.rs +++ /dev/null @@ -1,222 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use multiversx_price_aggregator_sc::{ - price_aggregator_data::{OracleStatus, TimestampedPrice, TokenPair}, - staking::StakingModule, - PriceAggregator, MAX_ROUND_DURATION_SECONDS, -}; -use multiversx_sc_scenario::{managed_address, managed_biguint, managed_buffer, rust_biguint}; - -mod price_agg_setup; -use price_agg_setup::*; - -#[test] -fn price_agg_submit_test() { - let mut pa_setup = PriceAggSetup::new(multiversx_price_aggregator_sc::contract_obj); - let current_timestamp = 100; - let oracles = pa_setup.oracles.clone(); - - // configure the number of decimals - pa_setup.set_pair_decimals(EGLD_TICKER, USD_TICKER, DECIMALS); - - // try submit while paused - pa_setup - .submit(&oracles[0], 99, 100) - .assert_user_error("Contract is paused"); - - // unpause - pa_setup.unpause(); - - // submit first timestamp too old - pa_setup - .submit(&oracles[0], 10, 100) - .assert_user_error("First submission too old"); - - // submit ok - pa_setup.submit(&oracles[0], 95, 100).assert_ok(); - - pa_setup - .b_mock - .execute_query(&pa_setup.price_agg, |sc| { - let token_pair = TokenPair { - from: managed_buffer!(EGLD_TICKER), - to: managed_buffer!(USD_TICKER), - }; - assert_eq!( - sc.first_submission_timestamp(&token_pair).get(), - current_timestamp - ); - assert_eq!( - sc.last_submission_timestamp(&token_pair).get(), - current_timestamp - ); - - let submissions = sc.submissions().get(&token_pair).unwrap(); - assert_eq!(submissions.len(), 1); - assert_eq!( - submissions.get(&managed_address!(&oracles[0])).unwrap(), - managed_biguint!(100) - ); - - assert_eq!( - sc.oracle_status() - .get(&managed_address!(&oracles[0])) - .unwrap(), - OracleStatus { - total_submissions: 1, - accepted_submissions: 1 - } - ); - }) - .assert_ok(); - - // first oracle submit again - submission not accepted - pa_setup.submit(&oracles[0], 95, 100).assert_ok(); - - pa_setup - .b_mock - .execute_query(&pa_setup.price_agg, |sc| { - assert_eq!( - sc.oracle_status() - .get(&managed_address!(&oracles[0])) - .unwrap(), - OracleStatus { - total_submissions: 2, - accepted_submissions: 1 - } - ); - }) - .assert_ok(); -} - -#[test] -fn price_agg_submit_round_ok_test() { - let mut pa_setup = PriceAggSetup::new(multiversx_price_aggregator_sc::contract_obj); - let oracles = pa_setup.oracles.clone(); - - // configure the number of decimals - pa_setup.set_pair_decimals(EGLD_TICKER, USD_TICKER, DECIMALS); - - // unpause - pa_setup.unpause(); - - // submit first - pa_setup.submit(&oracles[0], 95, 10_000).assert_ok(); - - let current_timestamp = 110; - pa_setup.b_mock.set_block_timestamp(current_timestamp); - - // submit second - pa_setup.submit(&oracles[1], 101, 11_000).assert_ok(); - - // submit third - pa_setup.submit(&oracles[2], 105, 12_000).assert_ok(); - - pa_setup - .b_mock - .execute_query(&pa_setup.price_agg, |sc| { - let result = sc - .latest_price_feed(managed_buffer!(EGLD_TICKER), managed_buffer!(USD_TICKER)) - .unwrap(); - - let (round_id, from, to, timestamp, price, decimals) = result.into_tuple(); - assert_eq!(round_id, 1); - assert_eq!(from, managed_buffer!(EGLD_TICKER)); - assert_eq!(to, managed_buffer!(USD_TICKER)); - assert_eq!(timestamp, current_timestamp); - assert_eq!(price, managed_biguint!(11_000)); - assert_eq!(decimals, DECIMALS); - - // submissions are deleted after round is created - let token_pair = TokenPair { from, to }; - let submissions = sc.submissions().get(&token_pair).unwrap(); - assert_eq!(submissions.len(), 0); - - let rounds = sc.rounds().get(&token_pair).unwrap(); - assert_eq!(rounds.len(), 1); - assert_eq!( - rounds.get(1), - TimestampedPrice { - timestamp, - price, - decimals - } - ); - }) - .assert_ok(); -} - -#[test] -fn price_agg_discarded_round_test() { - let mut pa_setup = PriceAggSetup::new(multiversx_price_aggregator_sc::contract_obj); - let oracles = pa_setup.oracles.clone(); - - // configure the number of decimals - pa_setup.set_pair_decimals(EGLD_TICKER, USD_TICKER, DECIMALS); - - // unpause - pa_setup.unpause(); - - // submit first - pa_setup.submit(&oracles[0], 95, 10_000).assert_ok(); - - let current_timestamp = 100 + MAX_ROUND_DURATION_SECONDS + 1; - pa_setup.b_mock.set_block_timestamp(current_timestamp); - - // submit second - this will discard the previous submission - pa_setup - .submit(&oracles[1], current_timestamp - 1, 11_000) - .assert_ok(); - - pa_setup - .b_mock - .execute_query(&pa_setup.price_agg, |sc| { - let token_pair = TokenPair { - from: managed_buffer!(EGLD_TICKER), - to: managed_buffer!(USD_TICKER), - }; - let submissions = sc.submissions().get(&token_pair).unwrap(); - assert_eq!(submissions.len(), 1); - assert_eq!( - submissions.get(&managed_address!(&oracles[1])).unwrap(), - managed_biguint!(11_000) - ); - }) - .assert_ok(); -} - -#[test] -fn price_agg_slashing_test() { - let rust_zero = rust_biguint!(0); - let mut pa_setup = PriceAggSetup::new(multiversx_price_aggregator_sc::contract_obj); - let oracles = pa_setup.oracles.clone(); - - // unpause - pa_setup.unpause(); - - pa_setup - .b_mock - .execute_tx(&oracles[0], &pa_setup.price_agg, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&oracles[1])); - }) - .assert_ok(); - - pa_setup - .b_mock - .execute_tx(&oracles[2], &pa_setup.price_agg, &rust_zero, |sc| { - sc.vote_slash_member(managed_address!(&oracles[1])); - }) - .assert_ok(); - - pa_setup - .b_mock - .execute_tx(&oracles[0], &pa_setup.price_agg, &rust_zero, |sc| { - sc.slash_member(managed_address!(&oracles[1])); - }) - .assert_ok(); - - // oracle 1 try submit after slashing - pa_setup - .submit(&oracles[1], 95, 10_000) - .assert_user_error("only oracles allowed"); -} diff --git a/contracts/core/price-aggregator/tests/price_aggregator_blackbox_test.rs b/contracts/core/price-aggregator/tests/price_aggregator_blackbox_test.rs new file mode 100644 index 0000000000..bc1078bb06 --- /dev/null +++ b/contracts/core/price-aggregator/tests/price_aggregator_blackbox_test.rs @@ -0,0 +1,388 @@ +use multiversx_price_aggregator_sc::{ + price_aggregator_data::{OracleStatus, TimestampedPrice, TokenPair}, + staking::ProxyTrait as _, + ContractObj, PriceAggregator, ProxyTrait as _, MAX_ROUND_DURATION_SECONDS, +}; +use multiversx_sc::{ + codec::multi_types::MultiValueVec, + types::{Address, EgldOrEsdtTokenIdentifier}, +}; +use multiversx_sc_modules::pause::ProxyTrait; +use multiversx_sc_scenario::{ + api::StaticApi, + managed_address, managed_biguint, managed_buffer, + scenario_model::{Account, AddressValue, ScCallStep, ScDeployStep, SetStateStep, TxExpect}, + ContractInfo, DebugApi, ScenarioWorld, WhiteboxContract, +}; + +const DECIMALS: u8 = 0; +const EGLD_TICKER: &[u8] = b"EGLD"; +const NR_ORACLES: usize = 4; +const OWNER_ADDRESS_EXPR: &str = "address:owner"; +const PRICE_AGGREGATOR_ADDRESS_EXPR: &str = "sc:price-aggregator"; +const PRICE_AGGREGATOR_PATH_EXPR: &str = "file:output/multiversx-price-aggregator-sc.wasm"; +const SLASH_AMOUNT: u64 = 10; +const SLASH_QUORUM: usize = 2; +const STAKE_AMOUNT: u64 = 20; +const SUBMISSION_COUNT: usize = 3; +const USD_TICKER: &[u8] = b"USDC"; + +type PriceAggregatorContract = ContractInfo>; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.set_current_dir_from_workspace("contracts/core/price-aggregator"); + blockchain.register_contract( + PRICE_AGGREGATOR_PATH_EXPR, + multiversx_price_aggregator_sc::ContractBuilder, + ); + + blockchain +} + +struct PriceAggregatorTestState { + world: ScenarioWorld, + oracles: Vec, + price_aggregator_contract: PriceAggregatorContract, + price_aggregator_whitebox: WhiteboxContract>, +} + +impl PriceAggregatorTestState { + fn new() -> Self { + let mut world = world(); + + let mut set_state_step = SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .new_address(OWNER_ADDRESS_EXPR, 1, PRICE_AGGREGATOR_ADDRESS_EXPR) + .block_timestamp(100); + + let mut oracles = Vec::new(); + for i in 1..=NR_ORACLES { + let address_expr = format!("address:oracle{}", i); + let address_value = AddressValue::from(address_expr.as_str()); + + set_state_step = set_state_step.put_account( + address_expr.as_str(), + Account::new().nonce(1).balance(STAKE_AMOUNT), + ); + + oracles.push(address_value); + } + world.set_state_step(set_state_step); + + let price_aggregator_contract = PriceAggregatorContract::new(PRICE_AGGREGATOR_ADDRESS_EXPR); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + + Self { + world, + oracles, + price_aggregator_contract, + price_aggregator_whitebox, + } + } + + fn deploy(&mut self) -> &mut Self { + let price_aggregator_code = self.world.code_expression(PRICE_AGGREGATOR_PATH_EXPR); + + let oracles = MultiValueVec::from( + self.oracles + .iter() + .map(|oracle| oracle.to_address()) + .collect::>(), + ); + + self.world.sc_deploy( + ScDeployStep::new() + .from(OWNER_ADDRESS_EXPR) + .code(price_aggregator_code) + .call(self.price_aggregator_contract.init( + EgldOrEsdtTokenIdentifier::egld(), + STAKE_AMOUNT, + SLASH_AMOUNT, + SLASH_QUORUM, + SUBMISSION_COUNT, + oracles, + )), + ); + + for address in self.oracles.iter() { + self.world.sc_call( + ScCallStep::new() + .from(address) + .egld_value(STAKE_AMOUNT) + .call(self.price_aggregator_contract.stake()), + ); + } + + self + } + + fn set_pair_decimals(&mut self) { + self.world.sc_call( + ScCallStep::new().from(OWNER_ADDRESS_EXPR).call( + self.price_aggregator_contract + .set_pair_decimals(EGLD_TICKER, USD_TICKER, DECIMALS), + ), + ); + } + + fn unpause_endpoint(&mut self) { + self.world.sc_call( + ScCallStep::new() + .from(OWNER_ADDRESS_EXPR) + .call(self.price_aggregator_contract.unpause_endpoint()), + ); + } + + fn submit(&mut self, from: &AddressValue, submission_timestamp: u64, price: u64) { + self.world.sc_call(ScCallStep::new().from(from).call( + self.price_aggregator_contract.submit( + EGLD_TICKER, + USD_TICKER, + submission_timestamp, + price, + DECIMALS, + ), + )); + } + + fn submit_and_expect_err( + &mut self, + from: &AddressValue, + submission_timestamp: u64, + price: u64, + err_message: &str, + ) { + self.world.sc_call( + ScCallStep::new() + .from(from) + .call(self.price_aggregator_contract.submit( + EGLD_TICKER, + USD_TICKER, + submission_timestamp, + price, + DECIMALS, + )) + .expect(TxExpect::user_error("str:".to_string() + err_message)), + ); + } + + fn vote_slash_member(&mut self, from: &AddressValue, member_to_slash: Address) { + self.world.sc_call( + ScCallStep::new().from(from).call( + self.price_aggregator_contract + .vote_slash_member(member_to_slash), + ), + ); + } +} + +#[test] +fn test_price_aggregator_submit() { + let mut state = PriceAggregatorTestState::new(); + state.deploy(); + + // configure the number of decimals + state.set_pair_decimals(); + + // try submit while paused + state.submit_and_expect_err(&state.oracles[0].clone(), 99, 100, "Contract is paused"); + + // unpause + state.unpause_endpoint(); + + // submit first timestamp too old + state.submit_and_expect_err( + &state.oracles[0].clone(), + 10, + 100, + "First submission too old", + ); + + // submit ok + state.submit(&state.oracles[0].clone(), 95, 100); + + let current_timestamp = 100; + state + .world + .whitebox_query(&state.price_aggregator_whitebox, |sc| { + let token_pair = TokenPair { + from: managed_buffer!(EGLD_TICKER), + to: managed_buffer!(USD_TICKER), + }; + assert_eq!( + sc.first_submission_timestamp(&token_pair).get(), + current_timestamp + ); + assert_eq!( + sc.last_submission_timestamp(&token_pair).get(), + current_timestamp + ); + + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 1); + assert_eq!( + submissions + .get(&managed_address!(&state.oracles[0].to_address())) + .unwrap(), + managed_biguint!(100) + ); + + assert_eq!( + sc.oracle_status() + .get(&managed_address!(&state.oracles[0].to_address())) + .unwrap(), + OracleStatus { + total_submissions: 1, + accepted_submissions: 1 + } + ); + }); + + // first oracle submit again - submission not accepted + state.submit(&state.oracles[0].clone(), 95, 100); + + state + .world + .whitebox_query(&state.price_aggregator_whitebox, |sc| { + assert_eq!( + sc.oracle_status() + .get(&managed_address!(&state.oracles[0].to_address())) + .unwrap(), + OracleStatus { + total_submissions: 2, + accepted_submissions: 1 + } + ); + }); +} + +#[test] +fn test_price_aggregator_submit_round_ok() { + let mut state = PriceAggregatorTestState::new(); + state.deploy(); + + // configure the number of decimals + state.set_pair_decimals(); + + // unpause + state.unpause_endpoint(); + + // submit first + state.submit(&state.oracles[0].clone(), 95, 10_000); + + let current_timestamp = 110; + state + .world + .set_state_step(SetStateStep::new().block_timestamp(current_timestamp)); + + // submit second + state.submit(&state.oracles[1].clone(), 101, 11_000); + + // submit third + state.submit(&state.oracles[2].clone(), 105, 12_000); + + state + .world + .whitebox_query(&state.price_aggregator_whitebox, |sc| { + let result = sc + .latest_price_feed(managed_buffer!(EGLD_TICKER), managed_buffer!(USD_TICKER)) + .unwrap(); + + let (round_id, from, to, timestamp, price, decimals) = result.into_tuple(); + assert_eq!(round_id, 1); + assert_eq!(from, managed_buffer!(EGLD_TICKER)); + assert_eq!(to, managed_buffer!(USD_TICKER)); + assert_eq!(timestamp, current_timestamp); + assert_eq!(price, managed_biguint!(11_000)); + assert_eq!(decimals, DECIMALS); + + // submissions are deleted after round is created + let token_pair = TokenPair { from, to }; + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 0); + + let rounds = sc.rounds().get(&token_pair).unwrap(); + assert_eq!(rounds.len(), 1); + assert_eq!( + rounds.get(1), + TimestampedPrice { + timestamp, + price, + decimals + } + ); + }); +} + +#[test] +fn test_price_aggregator_discarded_round() { + let mut state = PriceAggregatorTestState::new(); + state.deploy(); + + // configure the number of decimals + state.set_pair_decimals(); + + // unpause + state.unpause_endpoint(); + + // submit first + state.submit(&state.oracles[0].clone(), 95, 10_000); + + let current_timestamp = 100 + MAX_ROUND_DURATION_SECONDS + 1; + state + .world + .set_state_step(SetStateStep::new().block_timestamp(current_timestamp)); + + // submit second - this will discard the previous submission + state.submit(&state.oracles[1].clone(), current_timestamp - 1, 11_000); + + state + .world + .whitebox_query(&state.price_aggregator_whitebox, |sc| { + let token_pair = TokenPair { + from: managed_buffer!(EGLD_TICKER), + to: managed_buffer!(USD_TICKER), + }; + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 1); + assert_eq!( + submissions + .get(&managed_address!(&state.oracles[1].to_address())) + .unwrap(), + managed_biguint!(11_000) + ); + }); +} + +#[test] +fn test_price_aggregator_slashing() { + let mut state = PriceAggregatorTestState::new(); + state.deploy(); + + // unpause + state.unpause_endpoint(); + + state.vote_slash_member(&state.oracles[0].clone(), state.oracles[1].to_address()); + state.vote_slash_member(&state.oracles[2].clone(), state.oracles[1].to_address()); + + state.world.sc_call( + ScCallStep::new().from(&state.oracles[0]).call( + state + .price_aggregator_contract + .slash_member(state.oracles[1].to_address()), + ), + ); + + // oracle 1 try submit after slashing + state.submit_and_expect_err( + &state.oracles[1].clone(), + 95, + 10_000, + "only oracles allowed", + ); +} diff --git a/contracts/core/price-aggregator/tests/price_aggregator_whitebox_test.rs b/contracts/core/price-aggregator/tests/price_aggregator_whitebox_test.rs new file mode 100644 index 0000000000..a2ab56a22a --- /dev/null +++ b/contracts/core/price-aggregator/tests/price_aggregator_whitebox_test.rs @@ -0,0 +1,481 @@ +use multiversx_price_aggregator_sc::{ + price_aggregator_data::{OracleStatus, TimestampedPrice, TokenPair}, + staking::EndpointWrappers as StakingEndpointWrappers, + PriceAggregator, MAX_ROUND_DURATION_SECONDS, +}; +use multiversx_sc::types::{EgldOrEsdtTokenIdentifier, MultiValueEncoded}; +use multiversx_sc_modules::pause::EndpointWrappers as PauseEndpointWrappers; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_buffer, scenario_model::*, WhiteboxContract, *, +}; + +pub const DECIMALS: u8 = 0; +pub const EGLD_TICKER: &[u8] = b"EGLD"; +pub const NR_ORACLES: usize = 4; +pub const SLASH_AMOUNT: u64 = 10; +pub const SLASH_QUORUM: usize = 2; +pub const STAKE_AMOUNT: u64 = 20; +pub const SUBMISSION_COUNT: usize = 3; +pub const USD_TICKER: &[u8] = b"USDC"; + +const OWNER_ADDRESS_EXPR: &str = "address:owner"; +const PRICE_AGGREGATOR_ADDRESS_EXPR: &str = "sc:price-aggregator"; +const PRICE_AGGREGATOR_PATH_EXPR: &str = "file:output/multiversx-price-aggregator-sc.wasm"; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.set_current_dir_from_workspace("contracts/core/price-aggregator"); + blockchain.register_contract( + PRICE_AGGREGATOR_PATH_EXPR, + multiversx_price_aggregator_sc::ContractBuilder, + ); + + blockchain +} + +#[test] +fn test_price_aggregator_submit() { + let (mut world, oracles) = setup(); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + + // configure the number of decimals + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.set_pair_decimals( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + DECIMALS, + ) + }, + ); + + // try submit while paused + world.whitebox_call_check( + &price_aggregator_whitebox, + ScCallStep::new() + .from(&oracles[0]) + .expect(TxExpect::user_error("str:Contract is paused")), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 99, + managed_biguint!(100), + DECIMALS, + ) + }, + |r| r.assert_user_error("Contract is paused"), + ); + + // unpause + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| sc.call_unpause_endpoint(), + ); + + // submit first timestamp too old + world.whitebox_call_check( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[0]).no_expect(), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 10, + managed_biguint!(100), + DECIMALS, + ) + }, + |r| { + r.assert_user_error("First submission too old"); + }, + ); + + // submit ok + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[0]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 95, + managed_biguint!(100), + DECIMALS, + ) + }, + ); + + let current_timestamp = 100; + world.whitebox_query(&price_aggregator_whitebox, |sc| { + let token_pair = TokenPair { + from: managed_buffer!(EGLD_TICKER), + to: managed_buffer!(USD_TICKER), + }; + assert_eq!( + sc.first_submission_timestamp(&token_pair).get(), + current_timestamp + ); + assert_eq!( + sc.last_submission_timestamp(&token_pair).get(), + current_timestamp + ); + + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 1); + assert_eq!( + submissions + .get(&managed_address!(&oracles[0].to_address())) + .unwrap(), + managed_biguint!(100) + ); + + assert_eq!( + sc.oracle_status() + .get(&managed_address!(&oracles[0].to_address())) + .unwrap(), + OracleStatus { + total_submissions: 1, + accepted_submissions: 1 + } + ); + }); + + // first oracle submit again - submission not accepted + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[0]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 95, + managed_biguint!(100), + DECIMALS, + ) + }, + ); + + world.whitebox_query(&price_aggregator_whitebox, |sc| { + assert_eq!( + sc.oracle_status() + .get(&managed_address!(&oracles[0].to_address())) + .unwrap(), + OracleStatus { + total_submissions: 2, + accepted_submissions: 1 + } + ); + }); +} + +#[test] +fn test_price_aggregator_submit_round_ok() { + let (mut world, oracles) = setup(); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + + // configure the number of decimals + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.set_pair_decimals( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + DECIMALS, + ) + }, + ); + + // unpause + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| sc.call_unpause_endpoint(), + ); + + // submit first + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[0]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 95, + managed_biguint!(10_000), + DECIMALS, + ) + }, + ); + + let current_timestamp = 110; + world.set_state_step(SetStateStep::new().block_timestamp(current_timestamp)); + + // submit second + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[1]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 101, + managed_biguint!(11_000), + DECIMALS, + ) + }, + ); + + // submit third + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[2]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 105, + managed_biguint!(12_000), + DECIMALS, + ) + }, + ); + + world.whitebox_query(&price_aggregator_whitebox, |sc| { + let result = sc + .latest_price_feed(managed_buffer!(EGLD_TICKER), managed_buffer!(USD_TICKER)) + .unwrap(); + + let (round_id, from, to, timestamp, price, decimals) = result.into_tuple(); + assert_eq!(round_id, 1); + assert_eq!(from, managed_buffer!(EGLD_TICKER)); + assert_eq!(to, managed_buffer!(USD_TICKER)); + assert_eq!(timestamp, current_timestamp); + assert_eq!(price, managed_biguint!(11_000)); + assert_eq!(decimals, DECIMALS); + + // submissions are deleted after round is created + let token_pair = TokenPair { from, to }; + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 0); + + let rounds = sc.rounds().get(&token_pair).unwrap(); + assert_eq!(rounds.len(), 1); + assert_eq!( + rounds.get(1), + TimestampedPrice { + timestamp, + price, + decimals + } + ); + }); +} + +#[test] +fn test_price_aggregator_discarded_round() { + let (mut world, oracles) = setup(); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + + // configure the number of decimals + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + sc.set_pair_decimals( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + DECIMALS, + ) + }, + ); + + // unpause + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| sc.call_unpause_endpoint(), + ); + + // submit first + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[0]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 95, + managed_biguint!(10_000), + DECIMALS, + ) + }, + ); + + let current_timestamp = 100 + MAX_ROUND_DURATION_SECONDS + 1; + world.set_state_step(SetStateStep::new().block_timestamp(current_timestamp)); + + // submit second - this will discard the previous submission + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[1]), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + current_timestamp - 1, + managed_biguint!(11_000), + DECIMALS, + ) + }, + ); + + world.whitebox_query(&price_aggregator_whitebox, |sc| { + let token_pair = TokenPair { + from: managed_buffer!(EGLD_TICKER), + to: managed_buffer!(USD_TICKER), + }; + let submissions = sc.submissions().get(&token_pair).unwrap(); + assert_eq!(submissions.len(), 1); + assert_eq!( + submissions + .get(&managed_address!(&oracles[1].to_address())) + .unwrap(), + managed_biguint!(11_000) + ); + }); +} + +#[test] +fn test_price_aggregator_slashing() { + let (mut world, oracles) = setup(); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + + // unpause + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| sc.call_unpause_endpoint(), + ); + + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new() + .from(&oracles[0]) + .argument(BytesValue::from(oracles[1].to_address().as_bytes())), + |sc| sc.call_vote_slash_member(), + ); + + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new() + .from(&oracles[2]) + .argument(BytesValue::from(oracles[1].to_address().as_bytes())), + |sc| sc.call_vote_slash_member(), + ); + + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new() + .from(&oracles[0]) + .argument(BytesValue::from(oracles[1].to_address().as_bytes())), + |sc| sc.call_slash_member(), + ); + + // oracle 1 try submit after slashing + world.whitebox_call_check( + &price_aggregator_whitebox, + ScCallStep::new().from(&oracles[1]).no_expect(), + |sc| { + sc.submit( + managed_buffer!(EGLD_TICKER), + managed_buffer!(USD_TICKER), + 95, + managed_biguint!(10_000), + DECIMALS, + ) + }, + |r| { + r.assert_user_error("only oracles allowed"); + }, + ); +} + +fn setup() -> (ScenarioWorld, Vec) { + // setup + let mut world = world(); + let price_aggregator_whitebox = WhiteboxContract::new( + PRICE_AGGREGATOR_ADDRESS_EXPR, + multiversx_price_aggregator_sc::contract_obj, + ); + let price_aggregator_code = world.code_expression(PRICE_AGGREGATOR_PATH_EXPR); + + let mut set_state_step = SetStateStep::new() + .put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)) + .new_address(OWNER_ADDRESS_EXPR, 1, PRICE_AGGREGATOR_ADDRESS_EXPR) + .block_timestamp(100); + + let mut oracles = Vec::new(); + for i in 1..=NR_ORACLES { + let oracle_address_expr = format!("address:oracle{i}"); + let oracle_address = AddressValue::from(oracle_address_expr.as_str()); + + set_state_step = set_state_step.put_account( + oracle_address_expr.as_str(), + Account::new().nonce(1).balance(STAKE_AMOUNT), + ); + oracles.push(oracle_address); + } + + // init price aggregator + world.set_state_step(set_state_step).whitebox_deploy( + &price_aggregator_whitebox, + ScDeployStep::new() + .from(OWNER_ADDRESS_EXPR) + .code(price_aggregator_code), + |sc| { + let mut oracle_args = MultiValueEncoded::new(); + for oracle_address in &oracles { + oracle_args.push(managed_address!(&oracle_address.to_address())); + } + + sc.init( + EgldOrEsdtTokenIdentifier::egld(), + managed_biguint!(STAKE_AMOUNT), + managed_biguint!(SLASH_AMOUNT), + SLASH_QUORUM, + SUBMISSION_COUNT, + oracle_args, + ) + }, + ); + + for oracle_address in &oracles { + world.whitebox_call( + &price_aggregator_whitebox, + ScCallStep::new() + .from(oracle_address) + .egld_value(STAKE_AMOUNT), + |sc| sc.call_stake(), + ); + } + + (world, oracles) +} diff --git a/contracts/examples/multisig/tests/multisig_blackbox_test.rs b/contracts/examples/multisig/tests/multisig_blackbox_test.rs index 1ff87d1d37..b4e8765e3f 100644 --- a/contracts/examples/multisig/tests/multisig_blackbox_test.rs +++ b/contracts/examples/multisig/tests/multisig_blackbox_test.rs @@ -31,7 +31,6 @@ const OWNER_ADDRESS_EXPR: &str = "address:owner"; const PROPOSER_ADDRESS_EXPR: &str = "address:proposer"; const PROPOSER_BALANCE_EXPR: &str = "100,000,000"; const QUORUM_SIZE: usize = 1; -const STATUS_ERR_CODE_EXPR: u64 = 4; type MultisigContract = ContractInfo>; type AdderContract = ContractInfo>; @@ -243,10 +242,7 @@ impl MultisigTestState { ScCallStep::new() .from(BOARD_MEMBER_ADDRESS_EXPR) .call(self.multisig_contract.perform_action_endpoint(action_id)) - .expect(TxExpect::err( - STATUS_ERR_CODE_EXPR, - "str:".to_string() + err_message, - )), + .expect(TxExpect::user_error("str:".to_string() + err_message)), ); } @@ -370,8 +366,7 @@ fn test_change_quorum() { ScCallStep::new() .from(BOARD_MEMBER_ADDRESS_EXPR) .call(state.multisig_contract.discard_action(action_id)) - .expect(TxExpect::err( - STATUS_ERR_CODE_EXPR, + .expect(TxExpect::user_error( "str:cannot discard action with valid signatures", )), ); @@ -394,10 +389,7 @@ fn test_change_quorum() { ScCallStep::new() .from(BOARD_MEMBER_ADDRESS_EXPR) .call(state.multisig_contract.sign(action_id)) - .expect(TxExpect::err( - STATUS_ERR_CODE_EXPR, - "str:action does not exist", - )), + .expect(TxExpect::user_error("str:action does not exist")), ); // add another board member @@ -454,10 +446,7 @@ fn test_transfer_execute_to_user() { OptionalValue::::None, MultiValueVec::>::new(), )) - .expect(TxExpect::err( - STATUS_ERR_CODE_EXPR, - "str:proposed action has no effect", - )), + .expect(TxExpect::user_error("str:proposed action has no effect")), ); // propose diff --git a/framework/scenario/src/scenario/model/transaction/tx_expect.rs b/framework/scenario/src/scenario/model/transaction/tx_expect.rs index 9a49b6cc51..6f64a471f0 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_expect.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_expect.rs @@ -1,5 +1,4 @@ -use multiversx_chain_vm::tx_mock::result_values_to_string; - +use super::TxResponse; use crate::{ scenario::model::{BytesValue, CheckLogs, CheckValue, CheckValueList, U64Value}, scenario_format::{ @@ -8,8 +7,9 @@ use crate::{ }, scenario_model::Checkable, }; +use multiversx_chain_vm::tx_mock::result_values_to_string; -use super::TxResponse; +const USER_ERROR_CODE: u64 = 4; #[derive(Debug, Clone)] pub struct TxExpect { @@ -57,6 +57,13 @@ impl TxExpect { } } + pub fn user_error(err_msg_expr: E) -> Self + where + BytesValue: From, + { + Self::err(USER_ERROR_CODE, err_msg_expr) + } + pub fn no_result(mut self) -> Self { self.out = CheckValue::Equal(Vec::new()); self.build_from_response = false;