Skip to content

Commit daaae21

Browse files
authored
refactor: lmsr (#777)
Changed the implementation of LMSR. By using [LSE trick](https://gregorygundersen.com/blog/2020/02/09/log-sum-exp/), the calculation is much easier and numeric more stable. The calculated costs have a higher or same precision.
1 parent f095fde commit daaae21

File tree

4 files changed

+59
-55
lines changed

4 files changed

+59
-55
lines changed

pallets/pallet-bonded-coins/src/curves/lmsr.rs

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use substrate_fixed::{
88
};
99

1010
use super::BondingFunction;
11-
use crate::{PassiveSupply, Precision};
11+
use crate::{PassiveSupply, Precision, LOG_TARGET};
1212

1313
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
1414
pub struct LMSRParametersInput<Parameter> {
@@ -34,13 +34,30 @@ where
3434
Parameter: FixedSigned + PartialOrd<Precision> + From<Precision> + ToFixed,
3535
<Parameter as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign,
3636
{
37-
fn calculate_exp_term(&self, supply: Parameter, x: Parameter) -> Result<Parameter, ArithmeticError> {
38-
supply
39-
.checked_sub(x)
40-
.ok_or(ArithmeticError::Underflow)?
41-
.checked_div(self.m)
42-
.ok_or(ArithmeticError::DivisionByZero)
43-
.and_then(|exponent| exp::<Parameter, Parameter>(exponent).map_err(|_| ArithmeticError::Overflow))
37+
fn lse(&self, supply: &[Parameter]) -> Result<Parameter, ArithmeticError> {
38+
// Find the maximum value in the supply for numerical stability
39+
let max = supply.iter().max().ok_or_else(|| {
40+
log::error!(target: LOG_TARGET, "Supply is empty. Found pool with no currencies.");
41+
ArithmeticError::DivisionByZero
42+
})?;
43+
44+
// Compute the sum of the exponent terms, adjusted by max for stability
45+
let e_term_sum = supply.iter().try_fold(Parameter::from_num(0), |acc, x| {
46+
let exponent = x
47+
.checked_sub(*max)
48+
.ok_or(ArithmeticError::Underflow)?
49+
.checked_div(self.m)
50+
.ok_or(ArithmeticError::DivisionByZero)?;
51+
52+
let exp_result = exp::<Parameter, Parameter>(exponent).map_err(|_| ArithmeticError::Overflow)?;
53+
acc.checked_add(exp_result).ok_or(ArithmeticError::Overflow)
54+
})?;
55+
56+
// Compute the logarithm of the sum and scale it by `m`, then add the max term
57+
ln::<Parameter, Parameter>(e_term_sum)
58+
.map_err(|_| ArithmeticError::Underflow)
59+
.and_then(|log_sum| log_sum.checked_mul(self.m).ok_or(ArithmeticError::Overflow))
60+
.and_then(|scaled_log| scaled_log.checked_add(*max).ok_or(ArithmeticError::Overflow))
4461
}
4562
}
4663

@@ -49,48 +66,25 @@ where
4966
Parameter: FixedSigned + PartialOrd<Precision> + From<Precision> + ToFixed,
5067
<Parameter as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign,
5168
{
52-
// c(a, c) = (a - c) + b * ln((1 + SUM_i e^((q_i - a)/b)) / (1 + SUM_i e^((q_i - c)/b)))
5369
fn calculate_costs(
5470
&self,
5571
low: Parameter,
5672
high: Parameter,
5773
passive_supply: PassiveSupply<Parameter>,
5874
) -> Result<Parameter, ArithmeticError> {
59-
let e_term_numerator = passive_supply
60-
.iter()
61-
.map(|x| self.calculate_exp_term(*x, high))
62-
.collect::<Result<Vec<Parameter>, ArithmeticError>>()?;
63-
64-
let term1 = e_term_numerator.iter().try_fold(Parameter::from_num(0), |acc, x| {
65-
acc.checked_add(*x).ok_or(ArithmeticError::Overflow)
66-
})?;
67-
68-
let numerator = Parameter::from_num(1)
69-
.checked_add(term1)
70-
.ok_or(ArithmeticError::Overflow)?;
71-
72-
let e_term_denominator = passive_supply
73-
.iter()
74-
.map(|x| self.calculate_exp_term(*x, low))
75-
.collect::<Result<Vec<Parameter>, ArithmeticError>>()?;
76-
77-
let term2 = e_term_denominator.iter().try_fold(Parameter::from_num(0), |acc, x| {
78-
acc.checked_add(*x).ok_or(ArithmeticError::Overflow)
79-
})?;
80-
81-
let denominator = Parameter::from_num(1)
82-
.checked_add(term2)
83-
.ok_or(ArithmeticError::Overflow)?;
84-
85-
let log_value = numerator
86-
.checked_div(denominator)
87-
.ok_or(ArithmeticError::DivisionByZero)
88-
.and_then(|x| ln::<Parameter, Parameter>(x).map_err(|_| ArithmeticError::Overflow))?;
89-
90-
let high_low_diff = high.checked_sub(low).ok_or(ArithmeticError::Underflow)?;
75+
// Clone passive_supply and add low and high to create modified supplies
76+
let mut low_total_supply = passive_supply.clone();
77+
low_total_supply.push(low);
78+
let mut high_total_supply = passive_supply;
79+
high_total_supply.push(high);
9180

92-
let m_log_value = self.m.checked_mul(log_value).ok_or(ArithmeticError::Overflow)?;
81+
// Compute LSE for both modified supplies
82+
let lower_bound_value = self.lse(&low_total_supply)?;
83+
let high_bound_value = self.lse(&high_total_supply)?;
9384

94-
high_low_diff.checked_add(m_log_value).ok_or(ArithmeticError::Overflow)
85+
// Return the difference between high and low LSE values
86+
high_bound_value
87+
.checked_sub(lower_bound_value)
88+
.ok_or(ArithmeticError::Underflow)
9589
}
9690
}

pallets/pallet-bonded-coins/src/curves/mod.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ where
5656
}
5757
}
5858

59+
impl<Parameter> Curve<Parameter>
60+
where
61+
Parameter: FixedSigned + PartialOrd<Precision> + From<Precision> + ToFixed,
62+
<Parameter as Fixed>::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign,
63+
{
64+
fn as_inner(&self) -> &dyn BondingFunction<Parameter> {
65+
match self {
66+
Curve::Polynomial(params) => params,
67+
Curve::SquareRoot(params) => params,
68+
Curve::LMSR(params) => params,
69+
}
70+
}
71+
}
72+
5973
impl<Parameter> BondingFunction<Parameter> for Curve<Parameter>
6074
where
6175
Parameter: FixedSigned + PartialOrd<Precision> + From<Precision> + ToFixed,
@@ -67,11 +81,7 @@ where
6781
high: Parameter,
6882
passive_supply: PassiveSupply<Parameter>,
6983
) -> Result<Parameter, ArithmeticError> {
70-
match self {
71-
Curve::Polynomial(params) => params.calculate_costs(low, high, passive_supply),
72-
Curve::SquareRoot(params) => params.calculate_costs(low, high, passive_supply),
73-
Curve::Lmsr(params) => params.calculate_costs(low, high, passive_supply),
74-
}
84+
self.as_inner().calculate_costs(low, high, passive_supply)
7585
}
7686
}
7787

pallets/pallet-bonded-coins/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ pub mod pallet {
100100

101101
pub(crate) type TokenMetaOf<T> = TokenMeta<FungiblesBalanceOf<T>, CurrencyNameOf<T>, CurrencySymbolOf<T>>;
102102

103-
const LOG_TARGET: &str = "runtime::pallet-bonded-coins";
103+
pub(crate) const LOG_TARGET: &str = "runtime::pallet-bonded-coins";
104104

105105
/// Configure the pallet by specifying the parameters and types on which it depends.
106106
#[pallet::config]

pallets/pallet-bonded-coins/src/tests/curves/lmsr.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ fn high_supply_with_passive_issuance() {
6868

6969
let expected_costs = Float::from_str("0.0123394637").unwrap();
7070

71-
assert_relative_eq(costs, expected_costs, Float::from_str("0.00001").unwrap());
71+
assert_relative_eq(costs, expected_costs, Float::from_str("0.0000001").unwrap());
7272
}
7373

7474
// The main consequences of the low liquidity parameter is a lack of representable coins. e^40 goes beyond the representable range of the [Float] type.
@@ -107,12 +107,12 @@ fn mint_coin_with_existing_supply_and_no_passive_issuance() {
107107

108108
let passive_issuance = Float::from_num(0);
109109

110-
// Costs for existing supply: 100000000 * ln(e^(100/100000000) + e^(0/100000000)) = 69314768.056007030942
111-
// Costs for new supply: 100000000 * ln(e^(101/100000000) + e^(0/100000000)) = 69314768.556007282192
112-
// Costs to mint the first coin: 1000012340.2313117896 - 1000012340.2189723259 = 0.0123394637
110+
// Costs for existing supply: 100000000 * ln(e^(100/100000000) + e^(0/100000000)) = 69314768.056007030941723211624
111+
// Costs for new supply: 100000000 * ln(e^(101/100000000) + e^(0/100000000)) = 69314768.55600728219172321160383640
112+
// Costs to mint the first coin: 69314768.55600728219172321160383640 - 69314768.55600728219172321160383640 = 0.5000002412499999999798364
113113
let costs = curve.calculate_costs(low, high, vec![passive_issuance]).unwrap();
114114

115-
let expected_costs = Float::from_str("0.50000025125").unwrap();
115+
let expected_costs = Float::from_str("0.50000024125").unwrap();
116116

117117
assert_relative_eq(costs, expected_costs, Float::from_str("0.00000001").unwrap());
118118
}
@@ -137,5 +137,5 @@ fn mint_coin_with_existing_supply_and_passive_issuance() {
137137

138138
let expected_costs = Float::from_str("0.50000000125").unwrap();
139139

140-
assert_relative_eq(costs, expected_costs, Float::from_str("0.00000002").unwrap());
140+
assert_relative_eq(costs, expected_costs, Float::from_str("0.00000001").unwrap());
141141
}

0 commit comments

Comments
 (0)