diff --git a/Cargo.lock b/Cargo.lock index 158be3c..602046c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "ref-exchange" -version = "1.9.7" +version = "1.9.8" dependencies = [ "hex", "mock-boost-farming", diff --git a/mock-pyth/src/lib.rs b/mock-pyth/src/lib.rs index c4a815c..30fd31a 100644 --- a/mock-pyth/src/lib.rs +++ b/mock-pyth/src/lib.rs @@ -95,4 +95,14 @@ impl Contract { pub fn get_price(&self, price_identifier: PriceIdentifier) -> Option { self.price_info.get(&price_identifier).cloned() } + + pub fn list_prices_no_older_than(&self, price_ids: Vec, age: u64) -> HashMap> { + let _ = age; + let mut res = HashMap::new(); + for price_id in price_ids { + let price = self.price_info.get(&price_id).cloned(); + res.insert(price_id, price); + } + res + } } diff --git a/ref-exchange/Cargo.toml b/ref-exchange/Cargo.toml index 8f456de..6aa4a92 100644 --- a/ref-exchange/Cargo.toml +++ b/ref-exchange/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ref-exchange" -version = "1.9.7" +version = "1.9.8" authors = ["Illia Polosukhin "] edition = "2018" publish = false diff --git a/ref-exchange/release_notes.md b/ref-exchange/release_notes.md index d8599a0..7bcdd76 100644 --- a/ref-exchange/release_notes.md +++ b/ref-exchange/release_notes.md @@ -1,5 +1,12 @@ # Release Notes +### Version 1.9.8 +``` +CNFJRDekcistiyBHyZFif4CupZjND91VAj8hgR1E3Q35 +``` +1. add batch update degen price function +2. add token check to hotzap. + ### Version 1.9.7 ``` CMN4goNWHQjsXevLbqAC9nXKTw1yeJqysEfB647uuyro diff --git a/ref-exchange/src/degen_swap/degen.rs b/ref-exchange/src/degen_swap/degen.rs index 348ed12..2fe728d 100644 --- a/ref-exchange/src/degen_swap/degen.rs +++ b/ref-exchange/src/degen_swap/degen.rs @@ -1,5 +1,5 @@ -use super::price_oracle::{PriceOracleConfig, PriceOracleDegen}; -use super::pyth_oracle::{PythOracleConfig, PythOracleDegen}; +use super::price_oracle::{PriceOracleConfig, PriceOracleDegen, batch_update_degen_token_by_price_oracle}; +use super::pyth_oracle::{PythOracleConfig, PythOracleDegen, batch_update_degen_token_by_pyth_oracle}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; @@ -96,6 +96,13 @@ impl Degen { Degen::PythOracle(_) => "PythOracle".to_string(), } } + + pub fn update_price_info(&mut self, price_info: PriceInfo) { + match self { + Degen::PriceOracle(t) => t.price_info = Some(price_info), + Degen::PythOracle(t) => t.price_info = Some(price_info), + } + } } impl DegenTrait for Degen { @@ -222,6 +229,18 @@ pub fn global_unregister_degen_oracle_config(config_key: &String) -> bool { } } +pub fn global_update_degen(token_id: &AccountId, degen_type: DegenType) -> bool { + let mut degens = read_degens_from_storage(); + + if degens.contains_key(token_id) { + degens.insert(token_id.clone(), Degen::new(token_id.clone(), degen_type)); + write_degens_to_storage(degens); + true + } else { + false + } +} + pub fn global_update_degen_oracle_config(config: DegenOracleConfig) -> bool { let mut degen_oracle_configs = read_degen_oracle_configs_from_storage(); @@ -271,4 +290,27 @@ pub fn global_set_degen(token_id: &AccountId, degen: &Degen) { pub fn is_global_degen_price_valid(token_id: &AccountId) -> bool { init_degens_cache(); DEGENS.lock().unwrap().get(token_id).expect(format!("{} is not degen token", token_id).as_str()).is_price_valid() -} \ No newline at end of file +} + +// Both types of oracle-configured degen tokens can be updated simultaneously. +pub fn internal_batch_update_degen_token_price(token_ids: Vec) { + let mut token_id_decimals_map = HashMap::new(); + let mut price_id_token_id_map = HashMap::new(); + for token_id in token_ids { + let degen = global_get_degen(&token_id); + match degen { + Degen::PriceOracle(t) => { + token_id_decimals_map.insert(token_id, t.decimals); + }, + Degen::PythOracle(t) => { + price_id_token_id_map.insert(t.price_identifier.clone(), token_id); + }, + } + } + if !token_id_decimals_map.is_empty() { + batch_update_degen_token_by_price_oracle(token_id_decimals_map); + } + if !price_id_token_id_map.is_empty() { + batch_update_degen_token_by_pyth_oracle(price_id_token_id_map); + } +} diff --git a/ref-exchange/src/degen_swap/price_oracle.rs b/ref-exchange/src/degen_swap/price_oracle.rs index 82158e8..e44e259 100644 --- a/ref-exchange/src/degen_swap/price_oracle.rs +++ b/ref-exchange/src/degen_swap/price_oracle.rs @@ -1,7 +1,8 @@ +use crate::*; use super::global_get_degen_price_oracle_config; use super::{degen::DegenTrait, PRECISION}; use crate::errors::ERR126_FAILED_TO_PARSE_RESULT; -use crate::utils::{to_nano, u128_ratio, u64_dec_format, GAS_FOR_BASIC_OP, NO_DEPOSIT}; +use crate::utils::{u128_ratio, u64_dec_format, GAS_FOR_BASIC_OP, NO_DEPOSIT}; use crate::oracle::price_oracle; use crate::PriceInfo; use near_sdk::serde::{Deserialize, Serialize}; @@ -57,18 +58,7 @@ impl DegenTrait for PriceOracleDegen { let prices = from_slice::(cross_call_result).expect(ERR126_FAILED_TO_PARSE_RESULT); let timestamp = env::block_timestamp(); let config = global_get_degen_price_oracle_config(); - assert!( - prices.recency_duration_sec <= config.maximum_recency_duration_sec, - "Recency duration in the oracle call is larger than allowed maximum" - ); - assert!( - prices.timestamp <= timestamp, - "Price data timestamp is in the future" - ); - assert!( - timestamp - prices.timestamp <= to_nano(config.maximum_staleness_duration_sec), - "Price data timestamp is too stale" - ); + prices.assert_valid(timestamp, config.maximum_recency_duration_sec, config.maximum_staleness_duration_sec); assert!(prices.prices[0].asset_id == self.token_id, "Invalid price data"); let token_price = prices.prices[0].price.as_ref().expect("Missing token price"); @@ -81,4 +71,53 @@ impl DegenTrait for PriceOracleDegen { }); price } +} + +pub const GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PRICE_ORACLE_OP: Gas = 10_000_000_000_000; +pub const GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PRICE_ORACLE_CALLBACK: Gas = 10_000_000_000_000; + +// Batch retrieve the price oracle prices for degen tokens. +pub fn batch_update_degen_token_by_price_oracle(token_id_decimals_map: HashMap) { + let token_ids = token_id_decimals_map.keys().cloned().collect::>(); + let config = global_get_degen_price_oracle_config(); + price_oracle::ext_price_oracle::get_price_data( + Some(token_ids.clone()), + &config.oracle_id, + NO_DEPOSIT, + GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PRICE_ORACLE_OP + ).then(ext_self::batch_update_degen_token_by_price_oracle_callback( + token_id_decimals_map, + &env::current_account_id(), + NO_DEPOSIT, + GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PRICE_ORACLE_CALLBACK, + )); +} + +#[near_bindgen] +impl Contract { + // Invalid tokens do not affect the synchronization of valid tokens, and panic will not impact the swap. + #[private] + pub fn batch_update_degen_token_by_price_oracle_callback(&mut self, token_id_decimals_map: HashMap) { + if let Some(cross_call_result) = near_sdk::promise_result_as_success() { + let prices = from_slice::(&cross_call_result).expect(ERR126_FAILED_TO_PARSE_RESULT); + let timestamp = env::block_timestamp(); + let config = global_get_degen_price_oracle_config(); + prices.assert_valid(timestamp, config.maximum_recency_duration_sec, config.maximum_staleness_duration_sec); + for price_info in prices.prices { + if let Some(token_price) = price_info.price { + let token_id = price_info.asset_id; + if let Some(decimals) = token_id_decimals_map.get(&token_id) { + let mut degen = global_get_degen(&token_id); + let fraction_digits = 10u128.pow((token_price.decimals - decimals) as u32); + let price = u128_ratio(PRECISION, token_price.multiplier, fraction_digits as u128); + degen.update_price_info(PriceInfo { + stored_degen: price, + degen_updated_at: timestamp + }); + global_set_degen(&token_id, °en); + } + } + } + } + } } \ No newline at end of file diff --git a/ref-exchange/src/degen_swap/pyth_oracle.rs b/ref-exchange/src/degen_swap/pyth_oracle.rs index 8a12c65..30de088 100644 --- a/ref-exchange/src/degen_swap/pyth_oracle.rs +++ b/ref-exchange/src/degen_swap/pyth_oracle.rs @@ -1,3 +1,4 @@ +use crate::*; use super::global_get_degen_pyth_oracle_config; use super::{degen::DegenTrait, PRECISION}; use crate::errors::ERR126_FAILED_TO_PARSE_RESULT; @@ -64,4 +65,55 @@ impl DegenTrait for PythOracleDegen { }); price } +} + +pub const GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PYTH_ORACLE_OP: Gas = 15_000_000_000_000; +pub const GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PYTH_ORACLE_CALLBACK: Gas = 10_000_000_000_000; + +// Batch retrieve the pyth oracle prices for degen tokens. +pub fn batch_update_degen_token_by_pyth_oracle(price_id_token_id_map: HashMap) { + let price_ids = price_id_token_id_map.keys().cloned().collect::>(); + let config = global_get_degen_pyth_oracle_config(); + pyth_oracle::ext_pyth_oracle::list_prices_no_older_than( + price_ids, + config.pyth_price_valid_duration_sec as u64, + &config.oracle_id, + NO_DEPOSIT, + GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PYTH_ORACLE_OP + ).then(ext_self::batch_update_degen_token_by_pyth_oracle_callback( + price_id_token_id_map, + &env::current_account_id(), + NO_DEPOSIT, + GAS_FOR_BATCH_UPDATE_DEGEN_TOKEN_BY_PYTH_ORACLE_CALLBACK, + )); +} + +#[near_bindgen] +impl Contract { + // Invalid tokens do not affect the synchronization of valid tokens, and panic will not impact the swap. + #[private] + pub fn batch_update_degen_token_by_pyth_oracle_callback(&mut self, price_id_token_id_map: HashMap) { + if let Some(cross_call_result) = near_sdk::promise_result_as_success() { + let prices = from_slice::>>(&cross_call_result).expect(ERR126_FAILED_TO_PARSE_RESULT); + let timestamp = env::block_timestamp(); + let config = global_get_degen_pyth_oracle_config(); + for (price_id, token_id) in price_id_token_id_map { + if let Some(Some(price)) = prices.get(&price_id) { + if price.is_valid(timestamp, config.pyth_price_valid_duration_sec) { + let mut degen = global_get_degen(&token_id); + let price = if price.expo > 0 { + U256::from(PRECISION) * U256::from(price.price.0) * U256::from(10u128.pow(price.expo.abs() as u32)) + } else { + U256::from(PRECISION) * U256::from(price.price.0) / U256::from(10u128.pow(price.expo.abs() as u32)) + }.as_u128(); + degen.update_price_info(PriceInfo { + stored_degen: price, + degen_updated_at: timestamp + }); + global_set_degen(&token_id, °en); + } + } + } + } + } } \ No newline at end of file diff --git a/ref-exchange/src/lib.rs b/ref-exchange/src/lib.rs index 0074adc..6158b68 100644 --- a/ref-exchange/src/lib.rs +++ b/ref-exchange/src/lib.rs @@ -100,6 +100,8 @@ impl fmt::Display for RunningState { pub trait SelfCallbacks { fn update_token_rate_callback(&mut self, token_id: AccountId); fn update_degen_token_price_callback(&mut self, token_id: AccountId); + fn batch_update_degen_token_by_price_oracle_callback(&mut self, token_id_decimals_map: HashMap); + fn batch_update_degen_token_by_pyth_oracle_callback(&mut self, price_id_token_id_map: HashMap); } #[near_bindgen] @@ -593,6 +595,15 @@ impl Contract { } } + /// anyone can trigger a batch update for degen tokens + /// + /// # Arguments + /// + /// * `token_ids` - List of token IDs. + pub fn batch_update_degen_token_price(&self, token_ids: Vec) { + internal_batch_update_degen_token_price(token_ids.into_iter().map(|v| v.into()).collect()); + } + /// anyone can trigger an update for some degen token pub fn update_degen_token_price(& self, token_id: ValidAccountId) { let caller = env::predecessor_account_id(); @@ -726,11 +737,8 @@ impl Contract { self.finalize_prev_swap_chain(account, prev_action, &result); } } - let degen_tokens = self.get_degen_tokens_in_actions(actions); - for token_id in degen_tokens { - let degen = global_get_degen(&token_id); - degen.sync_token_price(&token_id); - } + let degen_token_ids = self.get_degen_tokens_in_actions(actions).into_iter().collect::>(); + internal_batch_update_degen_token_price(degen_token_ids); result } diff --git a/ref-exchange/src/oracle.rs b/ref-exchange/src/oracle.rs index 479c575..2bac60f 100644 --- a/ref-exchange/src/oracle.rs +++ b/ref-exchange/src/oracle.rs @@ -1,4 +1,4 @@ -use crate::utils::{u128_dec_format, u64_dec_format}; +use crate::utils::{u128_dec_format, u64_dec_format, to_nano}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::{ext_contract, Balance, Timestamp}; @@ -32,6 +32,23 @@ pub mod price_oracle { pub prices: Vec, } + impl PriceData { + pub fn assert_valid(&self, timestamp: u64, maximum_recency_duration_sec: u32, maximum_staleness_duration_sec: u32) { + assert!( + self.recency_duration_sec <= maximum_recency_duration_sec, + "Recency duration in the oracle call is larger than allowed maximum" + ); + assert!( + self.timestamp <= timestamp, + "Price data timestamp is in the future" + ); + assert!( + timestamp - self.timestamp <= to_nano(maximum_staleness_duration_sec), + "Price data timestamp is too stale" + ); + } + } + #[ext_contract(ext_price_oracle)] pub trait ExtPriceOracle { fn get_price_data(&self, asset_ids: Option>) -> PriceData; @@ -55,6 +72,14 @@ pub mod pyth_oracle { pub publish_time: i64, } + impl Price { + pub fn is_valid(&self, timestamp: u64, pyth_price_valid_duration_sec: u32) -> bool { + self.price.0 > 0 && + self.publish_time > 0 && + to_nano(self.publish_time as u32 + pyth_price_valid_duration_sec) >= timestamp + } + } + #[derive(BorshDeserialize, BorshSerialize, PartialEq, Eq, Hash, Clone)] #[repr(transparent)] pub struct PriceIdentifier(pub [u8; 32]); @@ -122,5 +147,6 @@ pub mod pyth_oracle { #[ext_contract(ext_pyth_oracle)] pub trait ExtPythOracle { fn get_price(&self, price_identifier: PriceIdentifier) -> Option; + fn list_prices_no_older_than(&self, price_ids: Vec, age: u64) -> HashMap>; } } \ No newline at end of file diff --git a/ref-exchange/src/owner.rs b/ref-exchange/src/owner.rs index a448e96..7e01c0a 100644 --- a/ref-exchange/src/owner.rs +++ b/ref-exchange/src/owner.rs @@ -396,6 +396,19 @@ impl Contract { } } + /// Update degen token. Only owner can call. + #[payable] + pub fn update_degen_token(&mut self, token_id: ValidAccountId, degen_type: DegenType) { + assert_one_yocto(); + self.assert_owner(); + let token_id: AccountId = token_id.into(); + if global_update_degen(&token_id, degen_type.clone()) { + log!("Update degen token {} to {:?} type", token_id, degen_type); + } else { + env::panic(format!("Degen token {} not exist", token_id).as_bytes()); + } + } + /// Remove degen token. Only owner can call. #[payable] pub fn unregister_degen_token(&mut self, token_id: ValidAccountId) { diff --git a/ref-exchange/src/token_receiver.rs b/ref-exchange/src/token_receiver.rs index 983f35c..0b1f457 100644 --- a/ref-exchange/src/token_receiver.rs +++ b/ref-exchange/src/token_receiver.rs @@ -143,7 +143,17 @@ impl FungibleTokenReceiver for Contract { } => { assert!(hot_zap_actions.len() > 0 && add_liquidity_infos.len() > 0); let sender_id: AccountId = sender_id.into(); - let mut account = self.internal_unwrap_account(&sender_id); + let mut account = self.internal_unwrap_account(&sender_id); + let all_tokens = self.get_hot_zap_tokens(&hot_zap_actions, &add_liquidity_infos); + for token_id in all_tokens.iter() { + assert!( + self.is_whitelisted_token(token_id) + || account.get_balance(token_id).is_some(), + "{}", + ERR12_TOKEN_NOT_WHITELISTED + ); + } + self.assert_no_frozen_tokens(&all_tokens); let referral_id = referral_id.map(|x| x.to_string()); let out_amounts = self.internal_direct_actions( token_in, @@ -226,4 +236,18 @@ impl FungibleTokenReceiver for Contract { } } } +} + +impl Contract { + pub fn get_hot_zap_tokens(&self, hot_zap_actions: &Vec, add_liquidity_infos: &Vec) -> Vec { + let mut all_tokens = HashSet::new(); + for action in hot_zap_actions { + all_tokens.extend(action.tokens()); + } + for add_liquidity_info in add_liquidity_infos { + let pool = self.pools.get(add_liquidity_info.pool_id).expect(ERR85_NO_POOL); + all_tokens.extend(pool.tokens().to_vec()); + } + all_tokens.into_iter().collect::>() + } } \ No newline at end of file diff --git a/ref-exchange/src/views.rs b/ref-exchange/src/views.rs index 19b1570..72a463e 100644 --- a/ref-exchange/src/views.rs +++ b/ref-exchange/src/views.rs @@ -57,6 +57,27 @@ pub struct DegenTokenInfo { pub decimals: Option } +impl From<&Degen> for DegenTokenInfo { + fn from(v: &Degen) -> Self { + DegenTokenInfo { + degen_type: v.get_type(), + degen_price: v.get_price_info().stored_degen.into(), + last_update_ts: v.get_price_info().degen_updated_at.into(), + is_price_valid: v.is_price_valid(), + price_identifier: if let Degen::PythOracle(d) = v { + Some(d.price_identifier.clone()) + } else { + None + }, + decimals: if let Degen::PriceOracle(d) = v { + Some(d.decimals) + } else { + None + }, + } + } +} + #[derive(Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] #[cfg_attr(not(target_arch = "wasm32"), derive(Debug, PartialEq))] @@ -683,31 +704,27 @@ impl Contract { .collect() } + /// Batch retrieve DegenTokenInfo based on the specified token ID list. + /// + /// # Arguments + /// + /// * `token_ids` - List of token IDs. + pub fn batch_get_degen_tokens(&self, token_ids: Vec) -> HashMap> { + let degens = read_degens_from_storage(); + token_ids.into_iter().map(|t| { + let token_id: AccountId = t.into(); + (token_id.clone(), degens.get(&token_id).map(|v| v.into())) + }).collect() + } + pub fn list_degen_tokens(&self) -> HashMap { let degens = read_degens_from_storage(); degens .iter() - .map(|(k, v)| { - ( - k.clone(), - DegenTokenInfo { - degen_type: v.get_type(), - degen_price: v.get_price_info().stored_degen.into(), - last_update_ts: v.get_price_info().degen_updated_at.into(), - is_price_valid: v.is_price_valid(), - price_identifier: if let Degen::PythOracle(d) = v { - Some(d.price_identifier.clone()) - } else { - None - }, - decimals: if let Degen::PriceOracle(d) = v { - Some(d.decimals) - } else { - None - }, - } - ) - }) + .map(|(k, v)| ( + k.clone(), + v.into() + )) .collect() } @@ -854,6 +871,7 @@ impl Contract { pub fn predict_hot_zap( &self, referral_id: Option, + account_id: Option, token_in: ValidAccountId, amount_in: U128, hot_zap_actions: Vec, @@ -862,6 +880,27 @@ impl Contract { if hot_zap_actions.is_empty() || add_liquidity_infos.is_empty() { return None } + let all_tokens = self.get_hot_zap_tokens(&hot_zap_actions, &add_liquidity_infos); + if let Some(account_id) = account_id { + let account = self.internal_unwrap_account(&account_id.into()); + for token_id in all_tokens.iter() { + assert!( + self.is_whitelisted_token(token_id) + || account.get_balance(token_id).is_some(), + "{}", + ERR12_TOKEN_NOT_WHITELISTED + ); + } + } else { + for token_id in all_tokens.iter() { + assert!( + self.is_whitelisted_token(token_id), + "{}", + ERR12_TOKEN_NOT_WHITELISTED + ); + } + } + self.assert_no_frozen_tokens(&all_tokens); let mut pool_cache = HashMap::new(); let mut add_liquidity_predictions = vec![]; let mut token_cache = TokenCache::new(); diff --git a/ref-exchange/tests/test_degen_pool.rs b/ref-exchange/tests/test_degen_pool.rs index e17fc30..9188652 100644 --- a/ref-exchange/tests/test_degen_pool.rs +++ b/ref-exchange/tests/test_degen_pool.rs @@ -446,3 +446,100 @@ fn degen_limit() { out_come.assert_success(); println!("{:#?}", get_logs(&out_come)); } + +#[test] +fn batch_update_degen_token() { + let (root, owner, pool, _) = + setup_degen_pool( + vec![eth(), btc()], + vec![100000*ONE_ETH, 100000*ONE_BTC], + vec![18, 8], + 25, + 10000, + ); + let pyth_contract = setup_pyth_oracle(&root); + let block_timestamp = root.borrow_runtime().current_block().block_timestamp; + call!( + root, + pyth_contract.set_price(mock_pyth::PriceIdentifier(hex::decode("27e867f0f4f61076456d1a73b14c7edc1cf5cef4f4d6193a33424288f11bd0f4").unwrap().try_into().unwrap()), PythPrice { + price: 100000000.into(), + conf: 397570.into(), + expo: -8, + publish_time: nano_to_sec(block_timestamp) as i64, + }) + ).assert_success(); + let price_oracle_contract = setup_price_oracle(&root); + call!( + root, + price_oracle_contract.set_price_data(eth(), Price { + multiplier: 20000, + decimals: 22, + }) + ).assert_success(); + + call!( + owner, + pool.register_degen_oracle_config(DegenOracleConfig::PriceOracle(PriceOracleConfig { + oracle_id: price_oracle(), + expire_ts: 3600 * 10u64.pow(9), + maximum_recency_duration_sec: 90, + maximum_staleness_duration_sec: 90 + })), + deposit = 1 + ) + .assert_success(); + call!( + owner, + pool.register_degen_oracle_config(DegenOracleConfig::PythOracle(PythOracleConfig { + oracle_id: pyth_oracle(), + expire_ts: 3600 * 10u64.pow(9), + pyth_price_valid_duration_sec: 60 + })), + deposit = 1 + ) + .assert_success(); + call!( + owner, + pool.register_degen_token(to_va(eth()), DegenType::PriceOracle { decimals: 18 }), + deposit = 1 + ) + .assert_success(); + call!( + owner, + pool.register_degen_token(to_va(btc()), DegenType::PythOracle { price_identifier: ref_exchange::pyth_oracle::PriceIdentifier(hex::decode("27e867f0f4f61076456d1a73b14c7edc1cf5cef4f4d6193a33424288f11bd0f4").unwrap().try_into().unwrap()) }), + deposit = 1 + ) + .assert_success(); + + call!( + root, + pool.batch_update_degen_token_price(vec![to_va(btc()), to_va(eth())]), + deposit = 0 + ) + .assert_success(); + + println!("{:?}", view!(pool.list_degen_tokens()).unwrap_json::>()); + println!("{:?}", view!(pool.batch_get_degen_tokens(vec![to_va(btc())])).unwrap_json::>()); + + call!( + owner, + pool.update_degen_token(to_va(btc()), DegenType::PriceOracle { decimals: 8 }), + deposit = 1 + ) + .assert_success(); + + call!( + root, + price_oracle_contract.set_price_data(btc(), Price { + multiplier: 100, + decimals: 10, + }) + ).assert_success(); + call!( + root, + pool.batch_update_degen_token_price(vec![to_va(btc()), to_va(eth())]), + deposit = 0 + ) + .assert_success(); + println!("{:?}", view!(pool.batch_get_degen_tokens(vec![to_va(btc())])).unwrap_json::>()); +} diff --git a/ref-exchange/tests/test_hotzap.rs b/ref-exchange/tests/test_hotzap.rs index af52147..b640cb1 100644 --- a/ref-exchange/tests/test_hotzap.rs +++ b/ref-exchange/tests/test_hotzap.rs @@ -51,6 +51,7 @@ fn test_hotzap_simple_pool() { println!("remain usdt : {:?}", out_usdt_amount - add_liquidity_prediction.need_amounts[1].0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_dai.valid_account_id(), U128(to_yocto("2")), @@ -196,6 +197,7 @@ fn test_hotzap_simple_pool_add_two() { println!("remain usdt : {:?}", out_usdt_amount - add_liquidity_prediction.need_amounts[1].0 - add_liquidity_prediction2.need_amounts[1].0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_dai.valid_account_id(), U128(to_yocto("2")), @@ -399,6 +401,7 @@ fn test_hotzap_stable_pool() { println!("{:?}", view!(pool.predict_add_stable_liquidity(DAI_USDT_USDC, &vec![U128(5*ONE_DAI), U128(4750565), U128(4750565)])).unwrap_json::().0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_dai.valid_account_id(), U128(15*ONE_DAI), @@ -609,6 +612,7 @@ fn test_hotzap_stable_pool_add_two() { println!("{:?}", view!(pool.predict_add_stable_liquidity(DAI_USDT_USDC2, &vec![U128(5*ONE_DAI / 2), U128(4750565 / 2), U128(4750565 / 2)])).unwrap_json::().0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_dai.valid_account_id(), U128(15*ONE_DAI), @@ -812,6 +816,7 @@ fn test_hotzap_rate_pool() { println!("{:?}", view!(pool.predict_add_stable_liquidity(0, &vec![U128(to_yocto("5")), U128(4750565543517085367305631u128)])).unwrap_json::().0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_near.valid_account_id(), U128(to_yocto("10")), @@ -1007,6 +1012,7 @@ fn test_hotzap_rate_pool_add_two() { println!("{:?}", view!(pool.predict_add_stable_liquidity(2, &vec![U128(to_yocto("5") / 2), U128(4750565543517085367305631u128 / 2)])).unwrap_json::().0); println!("predict hot zap: {:?}", view!(pool.predict_hot_zap( + None, None, token_near.valid_account_id(), U128(to_yocto("10")), @@ -1109,4 +1115,120 @@ fn test_hotzap_rate_pool_add_two() { println!("after mft_transfer_all_call: {:?}", view!(mock_boost_farming.get_seed(seed_id)).unwrap_json::()); println!("view new_user deposit: {:?}", view!(pool.get_deposits(new_user.valid_account_id())).unwrap_json::>()); +} + +#[test] +fn test_hotzap_simple_pool_frozen_token() { + const DAI_ETH: u64 = 0; + const ETH_USDT: u64 = 1; + const DAI_USDT: u64 = 2; + let (root, owner, pool, token_dai, token_eth, token_usdt) = setup_pool_with_liquidity(); + let new_user = root.create_user("new_user".to_string(), to_yocto("100")); + call!( + new_user, + pool.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + call!( + new_user, + token_dai.mint(to_va(new_user.account_id.clone()), U128(to_yocto("10"))) + ) + .assert_success(); + + let out_eth_amount = view!(pool.get_return(DAI_ETH, to_va(token_dai.account_id().clone()), U128(to_yocto("1")), to_va(token_eth.account_id().clone()))).unwrap_json::().0; + let out_usdt_amount = view!(pool.get_return(DAI_USDT, to_va(token_dai.account_id().clone()), U128(to_yocto("1")), to_va(token_usdt.account_id().clone()))).unwrap_json::().0; + let add_liquidity_prediction = view!(pool.predict_add_simple_liquidity(ETH_USDT, &vec![U128(out_eth_amount), U128(out_usdt_amount)])).unwrap_json::(); + + let out_come = call!( + owner, + pool.extend_frozenlist_tokens(vec![to_va(dai())]), + deposit=1 + ); + out_come.assert_success(); + + let outcome = call!( + new_user, + token_dai.ft_transfer_call( + to_va(swap()), + to_yocto("2").into(), + None, + json!({ + "hot_zap_actions": [ + {"pool_id": 0, "token_in": "dai001", "amount_in":"1000000000000000000000000", "token_out":"eth002", "min_amount_out":"1"}, + {"pool_id": 2, "token_in": "dai001", "amount_in":"1000000000000000000000000", "token_out":"usdt", "min_amount_out":"1"} + ], + "add_liquidity_infos": vec![ + AddLiquidityInfo { + pool_id: 1, + amounts: add_liquidity_prediction.need_amounts.clone(), + min_amounts: Some(vec![U128(1814048647419868151852681u128), U128(907024323709934075926341u128)]), + min_shares: None + } + ], + }).to_string() + ), + 1, + near_sdk_sim::DEFAULT_GAS + ); + let exe_status = format!("{:?}", outcome.promise_errors()[0].as_ref().unwrap().status()); + assert!(exe_status.contains("E52: token frozen")); +} + +#[test] +fn test_hotzap_simple_pool_not_whitelisted_token() { + const DAI_ETH: u64 = 0; + const ETH_USDT: u64 = 1; + const DAI_USDT: u64 = 2; + let (root, owner, pool, token_dai, token_eth, token_usdt) = setup_pool_with_liquidity(); + let new_user = root.create_user("new_user".to_string(), to_yocto("100")); + call!( + new_user, + pool.storage_deposit(None, None), + deposit = to_yocto("1") + ) + .assert_success(); + call!( + new_user, + token_dai.mint(to_va(new_user.account_id.clone()), U128(to_yocto("10"))) + ) + .assert_success(); + + let out_eth_amount = view!(pool.get_return(DAI_ETH, to_va(token_dai.account_id().clone()), U128(to_yocto("1")), to_va(token_eth.account_id().clone()))).unwrap_json::().0; + let out_usdt_amount = view!(pool.get_return(DAI_USDT, to_va(token_dai.account_id().clone()), U128(to_yocto("1")), to_va(token_usdt.account_id().clone()))).unwrap_json::().0; + let add_liquidity_prediction = view!(pool.predict_add_simple_liquidity(ETH_USDT, &vec![U128(out_eth_amount), U128(out_usdt_amount)])).unwrap_json::(); + + let out_come = call!( + owner, + pool.remove_whitelisted_tokens(vec![to_va(dai())]), + deposit = 1 + ); + out_come.assert_success(); + + let outcome = call!( + new_user, + token_dai.ft_transfer_call( + to_va(swap()), + to_yocto("2").into(), + None, + json!({ + "hot_zap_actions": [ + {"pool_id": 0, "token_in": "dai001", "amount_in":"1000000000000000000000000", "token_out":"eth002", "min_amount_out":"1"}, + {"pool_id": 2, "token_in": "dai001", "amount_in":"1000000000000000000000000", "token_out":"usdt", "min_amount_out":"1"} + ], + "add_liquidity_infos": vec![ + AddLiquidityInfo { + pool_id: 1, + amounts: add_liquidity_prediction.need_amounts.clone(), + min_amounts: Some(vec![U128(1814048647419868151852681u128), U128(907024323709934075926341u128)]), + min_shares: None + } + ], + }).to_string() + ), + 1, + near_sdk_sim::DEFAULT_GAS + ); + let exe_status = format!("{:?}", outcome.promise_errors()[0].as_ref().unwrap().status()); + assert!(exe_status.contains("E12: token not whitelisted")); } \ No newline at end of file diff --git a/ref-exchange/tests/test_migrate.rs b/ref-exchange/tests/test_migrate.rs index b534718..7bf7b26 100644 --- a/ref-exchange/tests/test_migrate.rs +++ b/ref-exchange/tests/test_migrate.rs @@ -51,7 +51,7 @@ fn test_upgrade() { .assert_success(); let metadata = get_metadata(&pool); // println!("{:#?}", metadata); - assert_eq!(metadata.version, "1.9.7".to_string()); + assert_eq!(metadata.version, "1.9.8".to_string()); assert_eq!(metadata.admin_fee_bps, 5); assert_eq!(metadata.boost_farm_id, "boost_farm".to_string()); assert_eq!(metadata.burrowland_id, "burrowland".to_string()); diff --git a/releases/ref_exchange_release.wasm b/releases/ref_exchange_release.wasm index 7a2ca09..27c0dd6 100644 Binary files a/releases/ref_exchange_release.wasm and b/releases/ref_exchange_release.wasm differ diff --git a/releases/ref_exchange_release_v197.wasm b/releases/ref_exchange_release_v197.wasm new file mode 100644 index 0000000..7a2ca09 Binary files /dev/null and b/releases/ref_exchange_release_v197.wasm differ