Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

batch update degen price #111

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading