use crate::compat::Instant;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use crate::error::CorpFinanceError;
use crate::types::{with_metadata, ComputationOutput, Money, Rate};
use crate::CorpFinanceResult;
const NVT_UNDERVALUED_UPPER: Decimal = dec!(20);
const NVT_FAIR_UPPER: Decimal = dec!(65);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComparableProtocol {
pub name: String,
pub fdv: Money,
pub revenue: Money,
#[serde(skip_serializing_if = "Option::is_none")]
pub tvl: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fees: Option<Money>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenValuationInput {
pub token_name: String,
pub network_value: Money,
pub daily_transaction_volume: Money,
pub active_addresses: u64,
pub annual_protocol_revenue: Money,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_value_locked: Option<Money>,
pub token_supply: Decimal,
pub circulating_supply: Decimal,
pub discount_rate: Rate,
pub revenue_growth_rate: Rate,
pub terminal_growth_rate: Rate,
#[serde(skip_serializing_if = "Option::is_none")]
pub projection_years: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comparable_protocols: Option<Vec<ComparableProtocol>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelativeValuation {
pub median_fdv_revenue: Decimal,
#[serde(skip_serializing_if = "Option::is_none")]
pub median_fdv_tvl: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub median_fdv_fees: Option<Decimal>,
pub implied_value_revenue: Money,
#[serde(skip_serializing_if = "Option::is_none")]
pub implied_value_tvl: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implied_value_fees: Option<Money>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenValuationOutput {
pub nvt_ratio: Decimal,
pub nvt_signal: String,
pub metcalfe_value: Money,
pub metcalfe_premium_discount: Rate,
#[serde(skip_serializing_if = "Option::is_none")]
pub dcf_value: Option<Money>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dcf_per_token: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relative_valuation: Option<RelativeValuation>,
pub warnings: Vec<String>,
}
pub fn value_token(
input: &TokenValuationInput,
) -> CorpFinanceResult<ComputationOutput<TokenValuationOutput>> {
let start = Instant::now();
let mut warnings: Vec<String> = Vec::new();
validate_input(input)?;
let proj_years = input.projection_years.unwrap_or(5);
let annualized_tx_volume = input.daily_transaction_volume * dec!(365);
let nvt_ratio = if annualized_tx_volume.is_zero() {
warnings
.push("Daily transaction volume is zero; NVT ratio is undefined — set to zero".into());
Decimal::ZERO
} else {
input.network_value / annualized_tx_volume
};
let nvt_signal = classify_nvt(nvt_ratio, annualized_tx_volume.is_zero());
let n = Decimal::from(input.active_addresses);
let n_squared = n * n;
let metcalfe_value = if n_squared.is_zero() {
warnings.push("Active addresses is zero; Metcalfe value is undefined".into());
Decimal::ZERO
} else {
let k = compute_metcalfe_coefficient(input, &mut warnings);
k * n_squared
};
let metcalfe_premium_discount = if metcalfe_value.is_zero() {
Decimal::ZERO
} else {
(input.network_value - metcalfe_value) / metcalfe_value
};
let (dcf_value, dcf_per_token) = if input.annual_protocol_revenue > Decimal::ZERO {
let dcf = compute_token_dcf(
input.annual_protocol_revenue,
input.revenue_growth_rate,
input.discount_rate,
input.terminal_growth_rate,
proj_years,
);
let per_token = if input.circulating_supply.is_zero() {
warnings.push("Circulating supply is zero; DCF per token undefined".into());
Decimal::ZERO
} else {
dcf / input.circulating_supply
};
(Some(dcf), Some(per_token))
} else {
warnings.push("Annual protocol revenue is zero or negative; DCF valuation skipped".into());
(None, None)
};
let relative_valuation = compute_relative_valuation(input, &mut warnings);
let output = TokenValuationOutput {
nvt_ratio,
nvt_signal,
metcalfe_value,
metcalfe_premium_discount,
dcf_value,
dcf_per_token,
relative_valuation,
warnings: warnings.clone(),
};
let elapsed = start.elapsed().as_micros() as u64;
Ok(with_metadata(
"Token Valuation — NVT, Metcalfe, DCF, and relative comparable analysis",
&serde_json::json!({
"token_name": input.token_name,
"projection_years": proj_years,
"discount_rate": input.discount_rate.to_string(),
"terminal_growth_rate": input.terminal_growth_rate.to_string(),
"nvt_thresholds": {"undervalued": "<20", "fair": "20-65", "overvalued": ">65"},
"metcalfe_law": "V = k * n^2",
"dcf_method": "Gordon Growth terminal value, iterative discount factors",
}),
warnings,
elapsed,
output,
))
}
fn validate_input(input: &TokenValuationInput) -> CorpFinanceResult<()> {
if input.network_value < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "network_value".into(),
reason: "Network value cannot be negative".into(),
});
}
if input.daily_transaction_volume < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "daily_transaction_volume".into(),
reason: "Daily transaction volume cannot be negative".into(),
});
}
if input.annual_protocol_revenue < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "annual_protocol_revenue".into(),
reason: "Annual protocol revenue cannot be negative".into(),
});
}
if input.token_supply <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "token_supply".into(),
reason: "Token supply must be positive".into(),
});
}
if input.circulating_supply < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "circulating_supply".into(),
reason: "Circulating supply cannot be negative".into(),
});
}
if input.circulating_supply > input.token_supply {
return Err(CorpFinanceError::InvalidInput {
field: "circulating_supply".into(),
reason: "Circulating supply cannot exceed total supply".into(),
});
}
if input.discount_rate <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "discount_rate".into(),
reason: "Discount rate must be positive".into(),
});
}
if input.discount_rate <= input.terminal_growth_rate {
return Err(CorpFinanceError::InvalidInput {
field: "discount_rate".into(),
reason: "Discount rate must exceed terminal growth rate for Gordon Growth model".into(),
});
}
if input.terminal_growth_rate < Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "terminal_growth_rate".into(),
reason: "Terminal growth rate cannot be negative".into(),
});
}
if let Some(years) = input.projection_years {
if years == 0 {
return Err(CorpFinanceError::InvalidInput {
field: "projection_years".into(),
reason: "Projection years must be at least 1".into(),
});
}
}
Ok(())
}
fn classify_nvt(nvt_ratio: Decimal, volume_is_zero: bool) -> String {
if volume_is_zero {
"N/A — no transaction volume".to_string()
} else if nvt_ratio < NVT_UNDERVALUED_UPPER {
"Undervalued".to_string()
} else if nvt_ratio <= NVT_FAIR_UPPER {
"Fair".to_string()
} else {
"Overvalued".to_string()
}
}
fn compute_metcalfe_coefficient(
input: &TokenValuationInput,
_warnings: &mut Vec<String>,
) -> Decimal {
let n = Decimal::from(input.active_addresses);
let n_squared = n * n;
if n_squared.is_zero() {
return Decimal::ZERO;
}
input.network_value / n_squared
}
fn compute_token_dcf(
annual_revenue: Money,
growth_rate: Rate,
discount_rate: Rate,
terminal_growth_rate: Rate,
years: u32,
) -> Money {
let one_plus_g = Decimal::ONE + growth_rate;
let one_plus_r = Decimal::ONE + discount_rate;
let mut total_pv = Decimal::ZERO;
let mut projected_revenue = annual_revenue;
let mut discount_factor = Decimal::ONE;
for _t in 1..=years {
projected_revenue *= one_plus_g;
discount_factor *= one_plus_r;
total_pv += projected_revenue / discount_factor;
}
let terminal_revenue = projected_revenue * (Decimal::ONE + terminal_growth_rate);
let terminal_value = terminal_revenue / (discount_rate - terminal_growth_rate);
total_pv += terminal_value / discount_factor;
total_pv
}
fn compute_relative_valuation(
input: &TokenValuationInput,
warnings: &mut Vec<String>,
) -> Option<RelativeValuation> {
let comps = match &input.comparable_protocols {
Some(c) if !c.is_empty() => c,
_ => return None,
};
let fdv_revenue_multiples: Vec<Decimal> = comps
.iter()
.filter(|c| c.revenue > Decimal::ZERO)
.map(|c| c.fdv / c.revenue)
.collect();
if fdv_revenue_multiples.is_empty() {
warnings.push("No comparable protocols with positive revenue for FDV/Revenue".into());
return None;
}
let median_fdv_revenue = median_decimal(&fdv_revenue_multiples);
let implied_value_revenue = if input.annual_protocol_revenue > Decimal::ZERO {
input.annual_protocol_revenue * median_fdv_revenue
} else {
warnings.push(
"Target annual_protocol_revenue is zero; implied FDV/Revenue value is zero".into(),
);
Decimal::ZERO
};
let fdv_tvl_multiples: Vec<Decimal> = comps
.iter()
.filter_map(|c| {
c.tvl.and_then(|tvl| {
if tvl > Decimal::ZERO {
Some(c.fdv / tvl)
} else {
None
}
})
})
.collect();
let (median_fdv_tvl, implied_value_tvl) = if fdv_tvl_multiples.is_empty() {
(None, None)
} else {
let med = median_decimal(&fdv_tvl_multiples);
let implied = input.total_value_locked.map(|tvl| {
if tvl > Decimal::ZERO {
tvl * med
} else {
Decimal::ZERO
}
});
(Some(med), implied)
};
let fdv_fees_multiples: Vec<Decimal> = comps
.iter()
.filter_map(|c| {
c.fees.and_then(|fees| {
if fees > Decimal::ZERO {
Some(c.fdv / fees)
} else {
None
}
})
})
.collect();
let (median_fdv_fees, implied_value_fees) = if fdv_fees_multiples.is_empty() {
(None, None)
} else {
let med = median_decimal(&fdv_fees_multiples);
let implied = if input.annual_protocol_revenue > Decimal::ZERO {
Some(input.annual_protocol_revenue * med)
} else {
Some(Decimal::ZERO)
};
(Some(med), implied)
};
Some(RelativeValuation {
median_fdv_revenue,
median_fdv_tvl,
median_fdv_fees,
implied_value_revenue,
implied_value_tvl,
implied_value_fees,
})
}
fn median_decimal(values: &[Decimal]) -> Decimal {
let mut sorted = values.to_vec();
sorted.sort();
let len = sorted.len();
if len % 2 == 1 {
sorted[len / 2]
} else {
(sorted[len / 2 - 1] + sorted[len / 2]) / dec!(2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn standard_token_input() -> TokenValuationInput {
TokenValuationInput {
token_name: "TestProtocol".to_string(),
network_value: dec!(1_000_000_000), daily_transaction_volume: dec!(10_000_000), active_addresses: 500_000,
annual_protocol_revenue: dec!(50_000_000), total_value_locked: Some(dec!(2_000_000_000)), token_supply: dec!(1_000_000_000), circulating_supply: dec!(500_000_000), discount_rate: dec!(0.20), revenue_growth_rate: dec!(0.30), terminal_growth_rate: dec!(0.03), projection_years: Some(5),
comparable_protocols: None,
}
}
fn three_comparables() -> Vec<ComparableProtocol> {
vec![
ComparableProtocol {
name: "ProtocolA".to_string(),
fdv: dec!(2_000_000_000),
revenue: dec!(100_000_000),
tvl: Some(dec!(5_000_000_000)),
fees: Some(dec!(80_000_000)),
},
ComparableProtocol {
name: "ProtocolB".to_string(),
fdv: dec!(500_000_000),
revenue: dec!(25_000_000),
tvl: Some(dec!(1_000_000_000)),
fees: Some(dec!(20_000_000)),
},
ComparableProtocol {
name: "ProtocolC".to_string(),
fdv: dec!(3_000_000_000),
revenue: dec!(200_000_000),
tvl: Some(dec!(8_000_000_000)),
fees: Some(dec!(150_000_000)),
},
]
}
#[test]
fn test_nvt_ratio_undervalued() {
let mut input = standard_token_input();
input.network_value = dec!(1_000_000_000);
input.daily_transaction_volume = dec!(200_000_000);
let result = value_token(&input).unwrap();
let out = &result.result;
let expected_nvt = dec!(1_000_000_000) / (dec!(200_000_000) * dec!(365));
let diff = (out.nvt_ratio - expected_nvt).abs();
assert!(
diff < dec!(0.0001),
"NVT ratio should be ~{}, got {}",
expected_nvt,
out.nvt_ratio
);
assert_eq!(out.nvt_signal, "Undervalued");
}
#[test]
fn test_nvt_ratio_fair() {
let mut input = standard_token_input();
input.daily_transaction_volume = dec!(68_493);
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(
out.nvt_ratio >= dec!(20) && out.nvt_ratio <= dec!(65),
"NVT ratio {} should be in Fair range (20-65)",
out.nvt_ratio
);
assert_eq!(out.nvt_signal, "Fair");
}
#[test]
fn test_nvt_ratio_overvalued() {
let mut input = standard_token_input();
input.daily_transaction_volume = dec!(27_397);
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(
out.nvt_ratio > dec!(65),
"NVT ratio {} should be > 65 (Overvalued)",
out.nvt_ratio
);
assert_eq!(out.nvt_signal, "Overvalued");
}
#[test]
fn test_metcalfe_valuation() {
let input = standard_token_input();
let result = value_token(&input).unwrap();
let out = &result.result;
let n = Decimal::from(500_000u64);
let n_sq = n * n;
let k = dec!(1_000_000_000) / n_sq;
let expected_metcalfe = k * n_sq;
let diff = (out.metcalfe_value - expected_metcalfe).abs();
assert!(
diff < dec!(1),
"Metcalfe value should be ~{}, got {}",
expected_metcalfe,
out.metcalfe_value
);
assert!(
out.metcalfe_premium_discount.abs() < dec!(0.0001),
"Premium/discount should be ~0, got {}",
out.metcalfe_premium_discount
);
}
#[test]
fn test_token_dcf_5_year() {
let input = standard_token_input();
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(out.dcf_value.is_some(), "DCF value should be present");
let dcf = out.dcf_value.unwrap();
assert!(
dcf > dec!(700_000_000) && dcf < dec!(850_000_000),
"DCF value should be ~770M, got {}",
dcf
);
}
#[test]
fn test_dcf_per_token() {
let input = standard_token_input();
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(
out.dcf_per_token.is_some(),
"DCF per token should be present"
);
let per_token = out.dcf_per_token.unwrap();
let dcf = out.dcf_value.unwrap();
let expected = dcf / dec!(500_000_000);
let diff = (per_token - expected).abs();
assert!(
diff < dec!(0.000001),
"DCF per token should be {}, got {}",
expected,
per_token
);
assert!(
per_token > dec!(1.0) && per_token < dec!(2.0),
"DCF per token should be between $1 and $2, got {}",
per_token
);
}
#[test]
fn test_relative_valuation_three_comparables() {
let mut input = standard_token_input();
input.comparable_protocols = Some(three_comparables());
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(
out.relative_valuation.is_some(),
"Relative valuation should be present"
);
let rv = out.relative_valuation.as_ref().unwrap();
assert_eq!(
rv.median_fdv_revenue,
dec!(20),
"Median FDV/Revenue should be 20, got {}",
rv.median_fdv_revenue
);
assert_eq!(
rv.implied_value_revenue,
dec!(1_000_000_000),
"Implied FDV/Revenue value should be 1B, got {}",
rv.implied_value_revenue
);
assert!(rv.median_fdv_tvl.is_some());
assert_eq!(
rv.median_fdv_tvl.unwrap(),
dec!(0.4),
"Median FDV/TVL should be 0.4, got {}",
rv.median_fdv_tvl.unwrap()
);
assert!(rv.implied_value_tvl.is_some());
assert_eq!(
rv.implied_value_tvl.unwrap(),
dec!(800_000_000),
"Implied FDV/TVL value should be 800M, got {}",
rv.implied_value_tvl.unwrap()
);
assert!(rv.median_fdv_fees.is_some());
assert_eq!(
rv.median_fdv_fees.unwrap(),
dec!(25),
"Median FDV/Fees should be 25, got {}",
rv.median_fdv_fees.unwrap()
);
}
#[test]
fn test_zero_transaction_volume_warning() {
let mut input = standard_token_input();
input.daily_transaction_volume = Decimal::ZERO;
let result = value_token(&input).unwrap();
let out = &result.result;
assert_eq!(out.nvt_ratio, Decimal::ZERO);
assert_eq!(out.nvt_signal, "N/A — no transaction volume");
assert!(
out.warnings
.iter()
.any(|w| w.contains("transaction volume is zero")),
"Should warn about zero transaction volume"
);
}
#[test]
fn test_discount_rate_lte_terminal_growth_rate_error() {
let mut input = standard_token_input();
input.discount_rate = dec!(0.03);
input.terminal_growth_rate = dec!(0.03);
let result = value_token(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, reason } => {
assert_eq!(field, "discount_rate");
assert!(reason.contains("terminal growth rate"));
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_full_integration() {
let mut input = standard_token_input();
input.comparable_protocols = Some(three_comparables());
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(out.nvt_ratio > Decimal::ZERO);
assert!(!out.nvt_signal.is_empty());
assert!(out.metcalfe_value > Decimal::ZERO);
assert!(out.dcf_value.is_some());
assert!(out.dcf_value.unwrap() > Decimal::ZERO);
assert!(out.dcf_per_token.is_some());
assert!(out.dcf_per_token.unwrap() > Decimal::ZERO);
assert!(out.relative_valuation.is_some());
let rv = out.relative_valuation.as_ref().unwrap();
assert!(rv.median_fdv_revenue > Decimal::ZERO);
assert!(rv.implied_value_revenue > Decimal::ZERO);
assert!(result.methodology.contains("Token Valuation"));
assert_eq!(result.metadata.precision, "rust_decimal_128bit");
}
#[test]
fn test_invalid_negative_network_value() {
let mut input = standard_token_input();
input.network_value = dec!(-100);
let result = value_token(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "network_value");
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_zero_active_addresses() {
let mut input = standard_token_input();
input.active_addresses = 0;
let result = value_token(&input).unwrap();
let out = &result.result;
assert_eq!(out.metcalfe_value, Decimal::ZERO);
assert_eq!(out.metcalfe_premium_discount, Decimal::ZERO);
assert!(
out.warnings
.iter()
.any(|w| w.contains("Active addresses is zero")),
"Should warn about zero active addresses"
);
}
#[test]
fn test_dcf_skipped_zero_revenue() {
let mut input = standard_token_input();
input.annual_protocol_revenue = Decimal::ZERO;
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(out.dcf_value.is_none());
assert!(out.dcf_per_token.is_none());
assert!(
out.warnings
.iter()
.any(|w| w.contains("DCF valuation skipped")),
"Should warn about skipped DCF"
);
}
#[test]
fn test_no_comparables() {
let mut input = standard_token_input();
input.comparable_protocols = None;
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(
out.relative_valuation.is_none(),
"Relative valuation should be None without comparables"
);
}
#[test]
fn test_circulating_exceeds_total_supply() {
let mut input = standard_token_input();
input.circulating_supply = dec!(2_000_000_000);
input.token_supply = dec!(1_000_000_000);
let result = value_token(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "circulating_supply");
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_median_odd() {
let values = vec![dec!(10), dec!(30), dec!(20)];
assert_eq!(median_decimal(&values), dec!(20));
}
#[test]
fn test_median_even() {
let values = vec![dec!(10), dec!(20), dec!(30), dec!(40)];
assert_eq!(median_decimal(&values), dec!(25));
}
#[test]
fn test_dcf_1_year_projection() {
let mut input = standard_token_input();
input.projection_years = Some(1);
let result = value_token(&input).unwrap();
let out = &result.result;
assert!(out.dcf_value.is_some());
let dcf = out.dcf_value.unwrap();
assert!(
dcf > dec!(350_000_000) && dcf < dec!(420_000_000),
"1-year DCF should be ~382M, got {}",
dcf
);
}
#[test]
fn test_zero_projection_years_error() {
let mut input = standard_token_input();
input.projection_years = Some(0);
let result = value_token(&input);
assert!(result.is_err());
match result.unwrap_err() {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "projection_years");
}
other => panic!("Expected InvalidInput, got {:?}", other),
}
}
#[test]
fn test_metadata_populated() {
let input = standard_token_input();
let result = value_token(&input).unwrap();
assert!(!result.methodology.is_empty());
assert!(result.methodology.contains("Token Valuation"));
assert_eq!(result.metadata.precision, "rust_decimal_128bit");
assert!(!result.metadata.version.is_empty());
}
}