Skip to content

Commit

Permalink
Time block independent Safe Price
Browse files Browse the repository at this point in the history
  • Loading branch information
psorinionut committed Feb 4, 2025
1 parent 420269a commit 6bc70ca
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 19 deletions.
35 changes: 31 additions & 4 deletions dex/pair/src/safe_price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use multiversx_sc::codec::{NestedDecodeInput, TopDecodeInput};
use crate::{amm, config, errors::ERROR_SAFE_PRICE_CURRENT_INDEX};

pub type Round = u64;
pub type Timestamp = u64;

pub const DEFAULT_ROUND_SAVE_INTERVAL: u64 = 1;
pub const MAX_OBSERVATIONS: usize = 65_536; // 2^{16} records, to optimise binary search

#[derive(ManagedVecItem, Clone, TopEncode, NestedEncode, TypeAbi, Debug)]
Expand All @@ -15,6 +17,7 @@ pub struct PriceObservation<M: ManagedTypeApi> {
pub second_token_reserve_accumulated: BigUint<M>,
pub weight_accumulated: u64,
pub recording_round: Round,
pub recording_timestamp: Timestamp,
pub lp_supply_accumulated: BigUint<M>,
}

Expand All @@ -25,6 +28,7 @@ impl<M: ManagedTypeApi> Default for PriceObservation<M> {
second_token_reserve_accumulated: BigUint::zero(),
weight_accumulated: 0,
recording_round: 0,
recording_timestamp: 0,
lp_supply_accumulated: BigUint::zero(),
}
}
Expand All @@ -47,10 +51,10 @@ impl<M: ManagedTypeApi> NestedDecode for PriceObservation<M> {
let weight_accumulated = u64::dep_decode(input)?;
let recording_round = u64::dep_decode(input)?;

let lp_supply_accumulated = if !input.is_depleted() {
BigUint::dep_decode(input)?
let (recording_timestamp, lp_supply_accumulated) = if !input.is_depleted() {
(u64::dep_decode(input)?, BigUint::dep_decode(input)?)
} else {
BigUint::zero()
(0u64, BigUint::zero())
};

if !input.is_depleted() {
Expand All @@ -62,6 +66,7 @@ impl<M: ManagedTypeApi> NestedDecode for PriceObservation<M> {
second_token_reserve_accumulated,
weight_accumulated,
recording_round,
recording_timestamp,
lp_supply_accumulated,
})
}
Expand Down Expand Up @@ -100,7 +105,14 @@ pub trait SafePriceModule:
new_index = (safe_price_current_index % MAX_OBSERVATIONS) + 1;
}

if last_price_observation.recording_round == current_round {
let rounds_since_last_observation = current_round - last_price_observation.recording_round;
let safe_price_round_save_interval_mapper = self.safe_price_round_save_interval();
let round_save_interval = match safe_price_round_save_interval_mapper.get() {
0 => DEFAULT_ROUND_SAVE_INTERVAL,
value => value,
};

if rounds_since_last_observation < round_save_interval {
return;
}

Expand Down Expand Up @@ -143,14 +155,29 @@ pub trait SafePriceModule:
new_price_observation.lp_supply_accumulated += BigUint::from(new_weight) * new_lp_supply;
new_price_observation.weight_accumulated += new_weight;
new_price_observation.recording_round = new_round;
new_price_observation.recording_timestamp = self.blockchain().get_block_timestamp();

new_price_observation
}

#[only_owner]
#[endpoint(setSafePriceRoundSaveInterval)]
fn set_safe_price_round_save_interval(&self, new_interval: u64) {
require!(
new_interval > 0,
"Round save interval must be greater than 0"
);
self.safe_price_round_save_interval().set(new_interval);
}

#[storage_mapper("price_observations")]
fn price_observations(&self) -> VecMapper<PriceObservation<Self::Api>>;

#[view(getSafePriceCurrentIndex)]
#[storage_mapper("safe_price_current_index")]
fn safe_price_current_index(&self) -> SingleValueMapper<usize>;

#[view(getSafePriceRoundSaveInterval)]
#[storage_mapper("safe_price_round_save_interval")]
fn safe_price_round_save_interval(&self) -> SingleValueMapper<u64>;
}
129 changes: 118 additions & 11 deletions dex/pair/src/safe_price_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
};

pub const DEFAULT_SAFE_PRICE_ROUNDS_OFFSET: u64 = 10 * 60;
pub const SECONDS_PER_ROUND: u64 = 6;
pub const OFFSET_PRECISION_FACTOR: u64 = 1_000_000;

struct PriceObservationWeightedAmounts<M: ManagedTypeApi> {
weighted_first_token_reserve: BigUint<M>,
Expand Down Expand Up @@ -69,15 +69,33 @@ pub trait SafePriceViewModule:
timestamp_offset: u64,
liquidity: BigUint,
) -> MultiValue2<EsdtTokenPayment, EsdtTokenPayment> {
let current_round = self.blockchain().get_block_round();
let round_offset = timestamp_offset / SECONDS_PER_ROUND;
let current_timestamp = self.blockchain().get_block_timestamp();

require!(
round_offset > 0 && round_offset < current_round,
timestamp_offset > 0 && timestamp_offset < current_timestamp,
ERROR_PARAMETERS
);
let start_round = current_round - round_offset;

self.get_lp_tokens_safe_price(pair_address, start_round, current_round, liquidity)
let target_timestamp = current_timestamp - timestamp_offset;

let safe_price_current_index = self
.get_safe_price_current_index_mapper(pair_address.clone())
.get();
let price_observations = self.get_price_observation_mapper(pair_address.clone());

let target_observation = self.find_observation_by_timestamp(
target_timestamp,
safe_price_current_index,
&price_observations,
);

let current_round = self.blockchain().get_block_round();
self.get_lp_tokens_safe_price(
pair_address,
target_observation.recording_round,
current_round,
liquidity,
)
}

#[label("safe-price-view")]
Expand Down Expand Up @@ -189,14 +207,32 @@ pub trait SafePriceViewModule:
timestamp_offset: u64,
input_payment: EsdtTokenPayment,
) -> EsdtTokenPayment {
let current_round = self.blockchain().get_block_round();
let round_offset = timestamp_offset / SECONDS_PER_ROUND;
let current_timestamp = self.blockchain().get_block_timestamp();
require!(
round_offset > 0 && round_offset < current_round,
timestamp_offset > 0 && timestamp_offset < current_timestamp,
ERROR_PARAMETERS
);
let start_round = current_round - round_offset;
self.get_safe_price(pair_address, start_round, current_round, input_payment)

let target_timestamp = current_timestamp - timestamp_offset;

let safe_price_current_index = self
.get_safe_price_current_index_mapper(pair_address.clone())
.get();
let price_observations = self.get_price_observation_mapper(pair_address.clone());

let target_observation = self.find_observation_by_timestamp(
target_timestamp,
safe_price_current_index,
&price_observations,
);

let current_round = self.blockchain().get_block_round();
self.get_safe_price(
pair_address,
target_observation.recording_round,
current_round,
input_payment,
)
}

#[label("safe-price-view")]
Expand Down Expand Up @@ -461,9 +497,14 @@ pub trait SafePriceViewModule:
let lp_supply_sum = BigUint::from(left_weight) * left_observation.lp_supply_accumulated
+ BigUint::from(right_weight) * right_observation.lp_supply_accumulated;

let timestamp_sum = BigUint::from(left_weight) * left_observation.recording_timestamp
+ BigUint::from(right_weight) * right_observation.recording_timestamp;
let interpolated_timestamp = (timestamp_sum / weight_sum).to_u64().unwrap_or_default();

let first_token_reserve_accumulated = first_token_reserve_sum / weight_sum;
let second_token_reserve_accumulated = second_token_reserve_sum / weight_sum;
let lp_supply_accumulated = lp_supply_sum / weight_sum;

let weight_accumulated =
left_observation.weight_accumulated + search_round - left_observation.recording_round;

Expand All @@ -472,10 +513,76 @@ pub trait SafePriceViewModule:
second_token_reserve_accumulated,
weight_accumulated,
recording_round: search_round,
recording_timestamp: interpolated_timestamp,
lp_supply_accumulated,
}
}

fn find_observation_by_timestamp(
&self,
target_timestamp: u64,
current_index: usize,
price_observations: &VecMapper<Self::Api, PriceObservation<Self::Api>, ManagedAddress>,
) -> PriceObservation<Self::Api> {
require!(
!price_observations.is_empty(),
ERROR_SAFE_PRICE_OBSERVATION_DOES_NOT_EXIST
);

let last_observation = price_observations.get(current_index);
if last_observation.recording_timestamp <= target_timestamp {
return last_observation;
}

let mut search_index = 1;
let mut left_index;
let mut right_index;
let observation_at_index_1 = price_observations.get(search_index);

if observation_at_index_1.recording_timestamp <= target_timestamp {
left_index = search_index;
right_index = current_index - 1;
} else {
left_index = current_index + 1;
right_index = price_observations.len();
}

let mut closest_observation = observation_at_index_1.clone();
let mut min_timestamp_diff =
if target_timestamp > observation_at_index_1.recording_timestamp {
target_timestamp - observation_at_index_1.recording_timestamp
} else {
observation_at_index_1.recording_timestamp - target_timestamp
};

while left_index <= right_index {
search_index = (left_index + right_index) / 2;
let current_observation = price_observations.get(search_index);
let current_timestamp_diff =
if target_timestamp > current_observation.recording_timestamp {
target_timestamp - current_observation.recording_timestamp
} else {
current_observation.recording_timestamp - target_timestamp
};

if current_timestamp_diff < min_timestamp_diff {
min_timestamp_diff = current_timestamp_diff;
closest_observation = current_observation.clone();
}

match current_observation
.recording_timestamp
.cmp(&target_timestamp)
{
Ordering::Equal => return current_observation,
Ordering::Less => left_index = search_index + 1,
Ordering::Greater => right_index = search_index - 1,
}
}

closest_observation
}

fn compute_weighted_amounts(
&self,
first_price_observation: &PriceObservation<Self::Api>,
Expand Down
6 changes: 4 additions & 2 deletions dex/pair/wasm-pair-full/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

// Init: 1
// Upgrade: 1
// Endpoints: 63
// Endpoints: 65
// Async Callback (empty): 1
// Total number of exported functions: 66
// Total number of exported functions: 68

#![no_std]

Expand Down Expand Up @@ -44,7 +44,9 @@ multiversx_sc_wasm_adapter::endpoints! {
getTotalSupply => lp_token_supply
getInitialLiquidtyAdder => initial_liquidity_adder
getReserve => pair_reserve
setSafePriceRoundSaveInterval => set_safe_price_round_save_interval
getSafePriceCurrentIndex => safe_price_current_index
getSafePriceRoundSaveInterval => safe_price_round_save_interval
updateAndGetTokensForGivenPositionWithSafePrice => update_and_get_tokens_for_given_position_with_safe_price
updateAndGetSafePrice => update_and_get_safe_price
setLockingDeadlineEpoch => set_locking_deadline_epoch
Expand Down
6 changes: 4 additions & 2 deletions dex/pair/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

// Init: 1
// Upgrade: 1
// Endpoints: 54
// Endpoints: 56
// Async Callback (empty): 1
// Total number of exported functions: 57
// Total number of exported functions: 59

#![no_std]

Expand Down Expand Up @@ -44,7 +44,9 @@ multiversx_sc_wasm_adapter::endpoints! {
getTotalSupply => lp_token_supply
getInitialLiquidtyAdder => initial_liquidity_adder
getReserve => pair_reserve
setSafePriceRoundSaveInterval => set_safe_price_round_save_interval
getSafePriceCurrentIndex => safe_price_current_index
getSafePriceRoundSaveInterval => safe_price_round_save_interval
updateAndGetTokensForGivenPositionWithSafePrice => update_and_get_tokens_for_given_position_with_safe_price
updateAndGetSafePrice => update_and_get_safe_price
setLockingDeadlineEpoch => set_locking_deadline_epoch
Expand Down

0 comments on commit 6bc70ca

Please sign in to comment.