Skip to content

Commit

Permalink
New algorithm for selection of bins in CaviarNine
Browse files Browse the repository at this point in the history
  • Loading branch information
0xOmarA committed Jan 5, 2024
1 parent 0cd076d commit 4142513
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 27 deletions.
225 changes: 225 additions & 0 deletions blueprints/caviarnine-adapter/src/bin_selector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
//! A module implementing the logic for the selection of the bins to contribute
//! liquidity to based on the current active bin, the bin span, and the maximum
//! number of allowed bins.

/// Ticks are in the range [0, 54000].
const MAXIMUM_TICK_VALUE: usize = 54000;

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SelectedBins {
pub active_bin: u32,
pub lower_bins: Vec<u32>,
pub higher_bins: Vec<u32>,
}

/// Selects the bins that the positions should be made out of.
///
/// Given the pool's active bin, bin span, and the preferred number of bins we
/// wish to have, this function determines which bins the liquidity should go
/// to without determining the exact formation of the liquidity (e.g., flat or
/// triangle). This function is specifically suited to handle edge cases when
/// selecting the bin that could lead to failures, and handles such cases in a
/// correct graceful manner.
///
/// In simple cases where the active bin is in the middle of the bin range and
/// the bin span is small enough, the selection of the lower and higher bins is
/// simple enough: preferred number of bins (divided by 2) to the left and the
/// same on the right. This gives the caller the number of bins that they wish
/// to have on the left and the right of the active bin.
///
/// There are cases however where the number of lower bins can't be equal to the
/// number of higher bins. Specifically, cases when the active bin's value is
/// too small or too large or cases when the bin span is too large. In such
/// cases, this function attempts to compensate the other side. As an example,
/// if we wish to have 10 bins and can only have 2 lower bins, then the higher
/// bins will have 8, thus achieving the preferred number of lower and higher
/// bins specified by the caller.
///
/// There are cases when the proffered number of bins can not be achieved by the
/// function, specifically, cases when the bin span is too large that any bin to
/// the right or the left will be outside of the range is allowed bins. In such
/// cases, this function returns a number of left and right bins that is less
/// than the desired.
///
/// # Examples
///
/// This section has examples with concrete numbers to to explain the behavior
/// of this function better.
///
/// ## Example 1: Simple Case
///
/// * `active_bin`: 100
/// * `bin_span`: 10
/// * `preferred_total_number_of_higher_and_lower_bins`: 4
///
/// This function will return the following:
///
/// * `active_bin`: 100
/// * `lower_bins`: [90, 80]
/// * `higher_bins`: [110, 120]
///
/// ## Example 2: Left Skew
///
/// * `active_bin`: 20
/// * `bin_span`: 10
/// * `preferred_total_number_of_higher_and_lower_bins`: 6
///
/// This function will return the following:
///
/// * `active_bin`: 20
/// * `lower_bins`: [10, 0]
/// * `higher_bins`: [30, 40, 50, 60]
///
/// At this currently active bin, there can only exist 2 bins on the lower side.
/// Thus, these bins are selected and left's remaining share of the bins is
/// given to the right. This results in a total of 6 bins.
///
/// ## Example 3: Right Skew
///
/// * `active_bin`: 53980
/// * `bin_span`: 10
/// * `preferred_total_number_of_higher_and_lower_bins`: 6
///
/// This function will return the following:
///
/// * `active_bin`: 53980
/// * `lower_bins`: [53970, 53960, 53950, 53940]
/// * `higher_bins`: [53990, 54000]
///
/// At this currently active bin, there can only exist 2 bins on the higher
/// side. Thus, these bins are selected and right's remaining share of the bins
/// is given to the left. This results in a total of 6 bins.
///
/// Example 4: Bin Size too large
///
/// * `active_bin`: 27000
/// * `bin_span`: 54000
/// * `preferred_total_number_of_higher_and_lower_bins`: 6
///
/// This function will return the following:
///
/// * `active_bin`: 27000
/// * `lower_bins`: []
/// * `higher_bins`: []
///
/// Given this pool's bin span, we can not get any bins that are lower or higher
/// and so we return just the active bin and return no lower or higher bins.
///
/// Example 4: Bin Size too large with a skew
///
/// * `active_bin`: 54000
/// * `bin_span`: 30000
/// * `preferred_total_number_of_higher_and_lower_bins`: 6
///
/// This function will return the following:
///
/// * `active_bin`: 54000
/// * `lower_bins`: [24000]
/// * `higher_bins`: []
///
/// # Arguments:
///
/// * `active_bin`: [`u32`] - The pool's currently active bin.
/// * `bin_span`: [`u32`] - The span between each bin and another or the
/// distance between them.
/// * `preferred_total_number_of_higher_and_lower_bins`: [`u32`] - The total
/// number of bins the caller wishes to have on the right and the left (summed).
/// As detailed above, this may or may not be achieved depending on the pool's
/// current bin and bin span.
///
/// # Returns:
///
/// [`SelectedBins`] - A struct with the bins that have been selected by this
/// function.
pub fn select_bins(
active_bin: u32,
bin_span: u32,
preferred_total_number_of_higher_and_lower_bins: u32,
) -> SelectedBins {
let mut selected_bins = SelectedBins {
active_bin,
higher_bins: vec![],
lower_bins: vec![],
};

let mut remaining = preferred_total_number_of_higher_and_lower_bins;

let mut forward_counter = BoundedU32::<0, MAXIMUM_TICK_VALUE>(active_bin);
let mut backward_counter = BoundedU32::<0, MAXIMUM_TICK_VALUE>(active_bin);

while remaining > 0 {
let mut forward_counter_incremented = false;
let mut backward_counter_decremented = false;

if forward_counter.checked_add_assign(bin_span).is_some() {
remaining -= 1;
selected_bins.higher_bins.push(forward_counter.0);
forward_counter_incremented = true;
}
if remaining > 0
&& backward_counter.checked_sub_assign(bin_span).is_some()
{
remaining -= 1;
selected_bins.lower_bins.push(backward_counter.0);
backward_counter_decremented = true;
}

if !forward_counter_incremented && !backward_counter_decremented {
break;
}
}

selected_bins
}

struct BoundedU32<const MIN: usize, const MAX: usize>(u32);

impl<const MIN: usize, const MAX: usize> BoundedU32<MIN, MAX> {
pub fn checked_add_assign(&mut self, other: impl Into<u32>) -> Option<()> {
if let Some(value) = self.checked_add(other) {
*self = value;
Some(())
} else {
None
}
}

pub fn checked_sub_assign(&mut self, other: impl Into<u32>) -> Option<()> {
if let Some(value) = self.checked_sub(other) {
*self = value;
Some(())
} else {
None
}
}

pub fn checked_add(&self, other: impl Into<u32>) -> Option<Self> {
let this = self.0;
let other = other.into();

if let Some(result) = this.checked_add(other) {
if result as usize > MAX {
None
} else {
Some(Self(result))
}
} else {
None
}
}

pub fn checked_sub(&self, other: impl Into<u32>) -> Option<Self> {
let this = self.0;
let other = other.into();

if let Some(result) = this.checked_sub(other) {
if (result as usize).lt(&MIN) {
None
} else {
Some(Self(result))
}
} else {
None
}
}
}
62 changes: 35 additions & 27 deletions blueprints/caviarnine-adapter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@
//! Bins with ticks lower than that of the active tick only have the `y` asset,
//! and the opposite for the other side. So, the `x` resources will be divided
//! across 100 bins and the y resources will be divided across another 100 bins.
//!
//! The bin selection algorithm tries to select 199 bins with the currently
//! active bin in the center, 98 lower bins, and 98 higher bins. However, there
//! are certain cases when this is not possible such as cases when the active
//! bin is skewed in one direction or another. In such case, the algorithm tries
//! to compensate on the other non-skewed side and attempts to get us 98 lower
//! and 98 higher. In cases when the bin span is too large, it is possible that
//! we get less 199 bins.

mod bin_selector;
pub use bin_selector::*;

use adapters_interface::pool::*;
use scrypto::prelude::*;
Expand Down Expand Up @@ -145,8 +156,6 @@ define_interface! {
}
}

const MAXIMUM_TICK_VALUE: u32 = 54000;

#[blueprint_with_traits]
mod adapter {
struct CaviarNineAdapter;
Expand Down Expand Up @@ -175,15 +184,6 @@ mod adapter {
impl PoolAdapterInterfaceTrait for CaviarNineAdapter {
// Opens 199 positions in flat formation. One at the current price, 99
// at the next 99 lower bins and 99 and the next 99 higher bins.

// TODO: Handle the case where the active bin is 0 or max. At 0 active
// bin we can't contribute to a lower bin, so we must contribute at
// current and above. At max we can't contribute to higher bins so we
// must contribute all to lower. Additionally, we should handle any
// active bin values that lead the lowest bin to be above the maximum.
// TODO: Handle the case when the bin span is large enough that we can't
// have 199 positions. This is possible as it seems like the CaviarNine
// protocol does not define a maximum for the bin span.
fn open_liquidity_position(
&mut self,
pool_address: ComponentAddress,
Expand Down Expand Up @@ -215,31 +215,38 @@ mod adapter {
let bin_span = pool.get_bin_span();
let active_bin =
pool.get_active_tick().expect("Pool has no active bin!");
let lower_bins = (1..=99)
.map(|multiplier| active_bin - bin_span * multiplier)
.collect::<Vec<_>>();
let higher_bins = (1..=99)
.map(|multiplier| active_bin + bin_span * multiplier)
.collect::<Vec<_>>();
let SelectedBins {
higher_bins,
lower_bins,
..
} = select_bins(active_bin, bin_span, 198);

// Determine the amount of resources that we will add to each of the
// bins. We have 99 on the left and 99 on the right. But, we alo
// bins. We have 99 on the left and 99 on the right. But, we also
// have the active bin that is composed of both x and y. So, this
// be like contributing to 99.x and 99.y bins where x = 1-y. X here
// is the ratio of resources x in the active bin.
let (active_bin_amount_x, active_bin_amount_y) =
let (amount_in_active_bin_x, amount_in_active_bin_y) =
pool.get_active_amounts().expect("No active amounts");
let price = pool.get_price().expect("No price");

let active_ratio_bin_x = active_bin_amount_x * price
/ (active_bin_amount_x * price + active_bin_amount_y);
let active_ratio_bin_y = Decimal::one() - active_ratio_bin_x;
let ratio_in_active_bin_x = amount_in_active_bin_x * price
/ (amount_in_active_bin_x * price + amount_in_active_bin_y);
let ratio_in_active_bin_y = Decimal::one() - ratio_in_active_bin_x;

// In here, we decide the amount x by the number of higher bins plus
// the ratio of the x in the currently active bin since the pool
// starting from the current price and upward is entirely composed
// of X. Similarly, we divide amount_y by the number of lower
// positions plus the ratio of y in the active bin since the pool
// starting from the current price and downward is composed just of
// y.
let position_amount_x = amount_x
/ (Decimal::from(higher_bins.len() as u32)
+ active_ratio_bin_x);
+ ratio_in_active_bin_x);
let position_amount_y = amount_y
/ (Decimal::from(lower_bins.len() as u32) + active_ratio_bin_y);
/ (Decimal::from(lower_bins.len() as u32)
+ ratio_in_active_bin_y);

// TODO: What?
let amount_bin_x_in_y = position_amount_x * price;
Expand All @@ -253,8 +260,8 @@ mod adapter {

let mut positions = vec![(
active_bin,
position_amount_x * active_ratio_bin_x,
position_amount_y * active_ratio_bin_y,
position_amount_x * ratio_in_active_bin_x,
position_amount_y * ratio_in_active_bin_y,
)];
positions.extend(
lower_bins
Expand All @@ -277,7 +284,8 @@ mod adapter {
change_y.resource_address() => change_y,
},
others: vec![],
// TODO: Determine how to plan to handle this in Caviar
// TODO: How do we plan on calculating the fees in C9? Can we
// use the same model as OciSwap.
pool_k: pdec!(0),
user_share: dec!(0),
}
Expand Down
2 changes: 2 additions & 0 deletions libraries/scrypto-interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ macro_rules! define_interface {
]
}

#[allow(clippy::too_many_arguments)]
impl [< $struct_ident InterfaceScryptoStub >] {
$crate::handle_functions_scrypto_stub!( $($functions)* );

Expand All @@ -137,6 +138,7 @@ macro_rules! define_interface {
}
}

#[allow(clippy::too_many_arguments)]
impl [< $struct_ident InterfaceScryptoTestStub >] {
$crate::handle_functions_scrypto_test_stub!( $($functions)* );

Expand Down
1 change: 1 addition & 0 deletions unit-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ radix-engine-store-interface = { workspace = true }
olympus = { path = "../blueprints/olympus", features = ["test"] }
test-oracle = { path = "../blueprints/test-oracle", features = ["test"] }
ociswap-adapter = { path = "../blueprints/ociswap-adapter", features = ["test"] }
caviarnine-adapter = { path = "../blueprints/caviarnine-adapter", features = ["test"] }
adapters-interface = { path = "../libraries/adapters-interface" }

paste = { version = "1.0.14" }
Expand Down
Loading

0 comments on commit 4142513

Please sign in to comment.