Skip to content

Commit

Permalink
1. add batch update degen price function (#111)
Browse files Browse the repository at this point in the history
2. add token check to hotzap.
  • Loading branch information
MagicGordon authored Oct 29, 2024
1 parent 4bf8df2 commit 85d2842
Show file tree
Hide file tree
Showing 17 changed files with 526 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions mock-pyth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,14 @@ impl Contract {
pub fn get_price(&self, price_identifier: PriceIdentifier) -> Option<PythPrice> {
self.price_info.get(&price_identifier).cloned()
}

pub fn list_prices_no_older_than(&self, price_ids: Vec<PriceIdentifier>, age: u64) -> HashMap<PriceIdentifier, Option<PythPrice>> {
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
}
}
2 changes: 1 addition & 1 deletion ref-exchange/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ref-exchange"
version = "1.9.7"
version = "1.9.8"
authors = ["Illia Polosukhin <[email protected]>"]
edition = "2018"
publish = false
Expand Down
7 changes: 7 additions & 0 deletions ref-exchange/release_notes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
48 changes: 45 additions & 3 deletions ref-exchange/src/degen_swap/degen.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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()
}
}

// Both types of oracle-configured degen tokens can be updated simultaneously.
pub fn internal_batch_update_degen_token_price(token_ids: Vec<AccountId>) {
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);
}
}
65 changes: 52 additions & 13 deletions ref-exchange/src/degen_swap/price_oracle.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -57,18 +58,7 @@ impl DegenTrait for PriceOracleDegen {
let prices = from_slice::<price_oracle::PriceData>(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");

Expand All @@ -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<AccountId, u8>) {
let token_ids = token_id_decimals_map.keys().cloned().collect::<Vec<_>>();
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<AccountId, u8>) {
if let Some(cross_call_result) = near_sdk::promise_result_as_success() {
let prices = from_slice::<price_oracle::PriceData>(&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, &degen);
}
}
}
}
}
}
52 changes: 52 additions & 0 deletions ref-exchange/src/degen_swap/pyth_oracle.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<pyth_oracle::PriceIdentifier, AccountId>) {
let price_ids = price_id_token_id_map.keys().cloned().collect::<Vec<_>>();
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<pyth_oracle::PriceIdentifier, AccountId>) {
if let Some(cross_call_result) = near_sdk::promise_result_as_success() {
let prices = from_slice::<HashMap<pyth_oracle::PriceIdentifier, Option<pyth_oracle::Price>>>(&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, &degen);
}
}
}
}
}
}
18 changes: 13 additions & 5 deletions ref-exchange/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountId, u8>);
fn batch_update_degen_token_by_pyth_oracle_callback(&mut self, price_id_token_id_map: HashMap<pyth_oracle::PriceIdentifier, AccountId>);
}

#[near_bindgen]
Expand Down Expand Up @@ -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<ValidAccountId>) {
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();
Expand Down Expand Up @@ -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::<Vec<_>>();
internal_batch_update_degen_token_price(degen_token_ids);
result
}

Expand Down
28 changes: 27 additions & 1 deletion ref-exchange/src/oracle.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -32,6 +32,23 @@ pub mod price_oracle {
pub prices: Vec<AssetOptionalPrice>,
}

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<Vec<AssetId>>) -> PriceData;
Expand All @@ -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]);
Expand Down Expand Up @@ -122,5 +147,6 @@ pub mod pyth_oracle {
#[ext_contract(ext_pyth_oracle)]
pub trait ExtPythOracle {
fn get_price(&self, price_identifier: PriceIdentifier) -> Option<Price>;
fn list_prices_no_older_than(&self, price_ids: Vec<PriceIdentifier>, age: u64) -> HashMap<PriceIdentifier, Option<Price>>;
}
}
13 changes: 13 additions & 0 deletions ref-exchange/src/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 85d2842

Please sign in to comment.