Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,11 @@ impl DistributionFunction {
}

// Calculate the denominator for the logarithm: m * (x - s + o)
let denom_f = (*m as f64) * (diff as f64);
let denom_f = if *m == 1 {
diff as f64
} else {
(*m as f64) * (diff as f64)
};
if denom_f <= 0.0 {
return Err(ProtocolError::Overflow(
"InvertedLogarithmic: computed denominator is non-positive",
Expand All @@ -437,26 +441,48 @@ impl DistributionFunction {

let log_val = argument.ln();

// Compute the final value: (a * ln(...)) / d + b.
let value = ((*a as f64) * log_val / (*d as f64)) + (*b as f64);
// Ensure the computed value is finite and within the u64 range.
if !log_val.is_finite() || log_val > (u64::MAX as f64) {
return Err(ProtocolError::Overflow(
"InvertedLogarithmic: evaluation overflow",
));
}

let intermediate = if *a == 1 {
log_val
} else if *a == -1 {
-log_val
} else {
(*a as f64) * log_val
};
if !intermediate.is_finite() || intermediate > (i64::MAX as f64) {
return Err(ProtocolError::Overflow(
"InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max",
));
}

let value = if d == &1 {
(intermediate.floor() as i64).checked_add(*b as i64).ok_or(
ProtocolError::Overflow(
"InvertedLogarithmic: evaluation overflow when adding b",
),
)?
} else {
((intermediate / (*d as f64)).floor() as i64)
.checked_add(*b as i64)
.ok_or(ProtocolError::Overflow(
"InvertedLogarithmic: evaluation overflow when adding b",
))?
};

// Clamp to max_value if provided.
if let Some(max_value) = max_value {
if value > *max_value as f64
|| (value.is_infinite() && value.is_sign_positive())
{
if value > *max_value as i64 {
return Ok(*max_value);
}
}

// Ensure the computed value is finite and within the u64 range.
if !value.is_finite() || value > (u64::MAX as f64) {
return Err(ProtocolError::Overflow(
"InvertedLogarithmic: evaluation overflow",
));
}

if value < 0.0 {
if value < 0 {
return if let Some(min_value) = min_value {
Ok(*min_value)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub const DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION

pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256;

pub const MIN_LOG_A_PARAM: i64 = -32_766;
pub const MAX_LOG_A_PARAM: i64 = 32_767;
pub const MAX_EXP_A_PARAM: u64 = 256;

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)]
pub enum DistributionFunction {
/// Emits a constant (fixed) number of tokens for every period.
Expand Down Expand Up @@ -499,22 +503,24 @@ pub enum DistributionFunction {
/// claimants receive diminishing rewards.
///
/// # Example
/// - Suppose a system starts with **500 tokens per period** and gradually reduces over time:
///
/// ```text
/// f(x) = (1000 * log(5000 / (5 * (x - 1000)))) / 10 + 10
/// f(x) = 10000 * log(5000 / x)
/// ```
///
/// Example values:
///
/// | Period (x) | Emission (f(x)) |
/// |------------|----------------|
/// | 1000 | 500 tokens |
/// | 1500 | 230 tokens |
/// | 2000 | 150 tokens |
/// | 5000 | 50 tokens |
/// | 10,000 | 20 tokens |
/// | 50,000 | 10 tokens |
/// - Values: a = 10000 n = 5000 m = 1 o = 0 b = 0 d = 0
/// y
/// ↑
/// 10000 |*
/// 9000 | *
/// 8000 | *
/// 7000 | *
/// 6000 | *
/// 5000 | *
/// 4000 | *
/// 3000 | *
/// 2000 | *
/// 1000 | *
/// 0 +-------------------*----------→ x
/// 0 2000 4000 6000 8000
///
/// - The emission **starts high** and **gradually decreases**, ensuring early adopters receive
/// more tokens while later participants still get rewards.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::consensus::basic::data_contract::{
InvalidTokenDistributionFunctionInvalidParameterTupleError,
};
use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::{
DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_LINEAR_SLOPE_PARAM,
DistributionFunction, MAX_DISTRIBUTION_PARAM, MAX_EXP_A_PARAM, MAX_LINEAR_SLOPE_PARAM,
MAX_LOG_A_PARAM, MIN_LOG_A_PARAM,
};
use crate::validation::SimpleConsensusValidationResult;
use crate::ProtocolError;
Expand Down Expand Up @@ -60,15 +61,15 @@ impl DistributionFunction {
step_count,
decrease_per_interval_numerator,
decrease_per_interval_denominator,
start_decreasing_offset: s,
start_decreasing_offset,
max_interval_count,
distribution_start_amount,
trailing_distribution_interval_amount,
min_value,
} => {
// Validate n.
if *distribution_start_amount == 0
|| *distribution_start_amount > MAX_DISTRIBUTION_PARAM as u64
|| *distribution_start_amount > MAX_DISTRIBUTION_PARAM
{
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidTokenDistributionFunctionInvalidParameterError::new(
Expand Down Expand Up @@ -149,7 +150,7 @@ impl DistributionFunction {
}
}

if let Some(s) = s {
if let Some(s) = start_decreasing_offset {
if *s > MAX_DISTRIBUTION_PARAM {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidTokenDistributionFunctionInvalidParameterError::new(
Expand Down Expand Up @@ -460,12 +461,13 @@ impl DistributionFunction {
.into(),
));
}
if *a == 0 {
// Check valid a values
if *a == 0 || *a > MAX_EXP_A_PARAM {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidTokenDistributionFunctionInvalidParameterError::new(
"a".to_string(),
1,
MAX_DISTRIBUTION_PARAM as i64,
MAX_LOG_A_PARAM,
None,
)
.into(),
Expand Down Expand Up @@ -638,13 +640,14 @@ impl DistributionFunction {
.into(),
));
}
if *a == 0 {
// Check valid a values
if *a == 0 || *a < MIN_LOG_A_PARAM || *a > MAX_LOG_A_PARAM {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidTokenDistributionFunctionInvalidParameterError::new(
"a".to_string(),
1,
MAX_DISTRIBUTION_PARAM as i64,
None,
MIN_LOG_A_PARAM,
MAX_LOG_A_PARAM,
Some(0),
)
.into(),
));
Expand Down Expand Up @@ -764,6 +767,18 @@ impl DistributionFunction {
min_value,
max_value,
} => {
// Check valid a values
if *a == 0 || *a < MIN_LOG_A_PARAM || *a > MAX_LOG_A_PARAM {
return Ok(SimpleConsensusValidationResult::new_with_error(
InvalidTokenDistributionFunctionInvalidParameterError::new(
"a".to_string(),
MIN_LOG_A_PARAM,
MAX_LOG_A_PARAM,
Some(0),
)
.into(),
));
}
// Check for division by zero.
if *d == 0 {
return Ok(SimpleConsensusValidationResult::new_with_error(
Expand Down Expand Up @@ -2209,6 +2224,66 @@ mod tests {
);
}

#[test]
fn test_inverted_logarithmic_invalid_zero_a() {
let dist = DistributionFunction::InvertedLogarithmic {
a: 0,
d: 1,
m: 1,
n: 100,
o: 1,
start_moment: Some(0),
b: 5,
min_value: Some(1),
max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary
};
let result = dist.validate(START_MOMENT);
assert_eq!(
result.expect("expected valid").first_error().expect("expected error").to_string(),
"Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)"
);
}

#[test]
fn test_inverted_logarithmic_invalid_too_low_a() {
let dist = DistributionFunction::InvertedLogarithmic {
a: -50000,
d: 1,
m: 1,
n: 100,
o: 1,
start_moment: Some(0),
b: 5,
min_value: Some(1),
max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary
};
let result = dist.validate(START_MOMENT);
assert_eq!(
result.expect("expected valid").first_error().expect("expected error").to_string(),
"Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)"
);
}

#[test]
fn test_inverted_logarithmic_invalid_too_high_a() {
let dist = DistributionFunction::InvertedLogarithmic {
a: 50000,
d: 1,
m: 1,
n: 100,
o: 1,
start_moment: Some(0),
b: 5,
min_value: Some(1),
max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary
};
let result = dist.validate(START_MOMENT);
assert_eq!(
result.expect("expected valid").first_error().expect("expected error").to_string(),
"Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)"
);
}

#[test]
fn test_inverted_logarithmic_invalid_divide_by_zero_d() {
let dist = DistributionFunction::InvertedLogarithmic {
Expand Down Expand Up @@ -2349,6 +2424,28 @@ mod tests {
);
}

#[test]
fn test_inverted_logarithmic_valid_with_min_a() {
// Since `a` is negative, the inverted logarithmic function is increasing,
// but it starts at the maximum value already, so it will never produce a higher value.
let dist = DistributionFunction::InvertedLogarithmic {
a: i64::MIN,
d: 1,
m: 1,
n: 100,
o: 1,
start_moment: Some(0),
b: 5,
min_value: Some(1),
max_value: Some(MAX_DISTRIBUTION_PARAM), // Valid max boundary
};
let result = dist.validate(START_MOMENT);
assert_eq!(
result.expect("expected valid").first_error().expect("expected error").to_string(),
"Invalid parameter `a` in token distribution function. Expected range: -32766 to 32767 except 0 (which we got)"
);
}

#[test]
fn test_inverted_logarithmic_invalid_starting_at_max_for_increasing() {
let dist = DistributionFunction::InvertedLogarithmic {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use bincode::{Decode, Encode};
#[derive(
Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize,
)]
#[error("Incoherent parameters in token distribution function: {}.", message)]
#[error("Incoherent parameters in token distribution function: {}", message)]
#[platform_serialize(unversioned)]
pub struct InvalidTokenDistributionFunctionIncoherenceError {
/*
Expand Down
Loading