Bug Bounty Submission: Risk-Scoring Vulnerability Report
Target Contract: contracts/risk-scoring (RiskScoringContract)
Reporter: teddyvj (teddy.vj@gmail.com)
Severity: High / Critical (Mathematical Protocol Breakdown, Invariant Violation, Broken Compilation)
1. Summary of Vulnerabilities
A. Double-Scaling Score Deflation (Permanent Grade 'D' Protocol Collapse)
In compute_risk_score:
let vol_score = if asset_volatility_bps < 1000 { 250 } ... else { 100 };
let oracle_score = if oracle_deviation_bps < 50 { 250 } ... else { 100 };
let util_score = if pool_utilization_bps < 6000 { 250 } ... else { 100 };
let liq_score = if liquidation_history_bps < 100 { 250 } ... else { 100 };
let weighted = (vol_score * weights.asset_volatility_weight
+ oracle_score * weights.oracle_deviation_weight
+ util_score * weights.pool_utilization_weight
+ liq_score * weights.liquidation_history_weight)
/ BPS_DIVISOR as u32;
The sub-scores were partitioned as if unweighted (max 250 each: 250 * 4 = 1000). However, they were then multiplied by basis-point weights summing to 10,000 (100%) and divided by BPS_DIVISOR (10,000).
This effectively divided the scores by 4 twice. The maximum possible weighted score achievable by any pool—even with zero volatility, zero oracle deviation, and zero liquidations—was:
(250 * 3000 + 250 * 2500 + 250 * 2500 + 250 * 2000) / 10000 = 250
In score_to_letter_grade:
let idx = if score >= 950 { 0 /* A+ */ } ... else if score >= 550 { 8 /* C- */ } else { 9 /* D */ };
Because the maximum score was capped at 250, any score >= 550 was mathematically impossible. Every single pool in the protocol was permanently evaluated as 'D' grade, regardless of safety or capital adequacy. Lending risk checks consuming this score would trigger false insolvency or refuse borrowing protocol-wide.
B. Compilation Failure via Invalid String Constructor
In score_to_letter_grade:
String::from_slice(&[], LETTER_GRADES[idx].as_bytes())
The contract attempted to construct a Soroban String by passing an empty slice reference &[] as &Env and raw bytes as a string slice, failing compilation in upstream main.
C. Pool Mismatch in record_pool_risk_score
record_pool_risk_score(env, pool, score) stored score under DataKey::PoolRiskScore(pool) without verifying that score.pool == pool, allowing inconsistent cross-pool score records.
D. Missing Authorization on initialize
initialize(env, admin) lacked admin.require_auth(), exposing the contract to front-running during initial deployment.
2. Remediation
- Normalized Sub-Scores to 1000-Point Scale: Scaled risk tiers to 1000, 800, 600, and 400. With normalized tiers, the weighted basis-point sum evaluates across the intended [400..1000] spectrum, correctly activating all letter grades (A+ down to D).
- Correct String Construction: Migrated to
String::from_str(&env, LETTER_GRADES[idx]).
- Pool Invariant Enforcement: Added
assert_eq!(score.pool, pool, "pool address mismatch") in record_pool_risk_score.
- Initialization Protection: Added
admin.require_auth() in initialize.
- Crate Configuration: Added
"lib" to crate-type in Cargo.toml to support standard cargo testing and integration.
3. Verification
All 11 unit tests pass with zero warnings:
$ cargo test -p risk-scoring
running 11 tests
test tests::test_default_weights_sum_to_10000 ... ok
test tests::test_double_initialize_panics - should panic ... ok
test tests::test_initialize ... ok
test tests::test_letter_grade_thresholds ... ok
test tests::test_record_pool_score_mismatch_panics - should panic ... ok
test tests::test_set_risk_weights ... ok
test tests::test_set_risk_weights_invalid_sum_panics - should panic ... ok
test tests::test_calculate_score_high_risk ... ok
test tests::test_calculate_score_medium_risk ... ok
test tests::test_calculate_score_low_risk ... ok
test tests::test_record_and_get_pool_score ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
4. Bounty Payout Information
In accordance with the repository's SECURITY.md Bug Bounty Program:
- EVM (USDC / USDT):
0xf5fcb1f90f8a2e658f38f72f0156ecbec7aa964d
- Solana (USDC / SOL):
ECYrMmKpVNyvWMuLNkBuWxqkk2TNRC3qVPDgonBwamKP
- Bitcoin (BTC):
bc1qjg5lug59rn9rz2j2f9ut798g99mn2asw9jfv0s
Bug Bounty Submission: Risk-Scoring Vulnerability Report
Target Contract:
contracts/risk-scoring(RiskScoringContract)Reporter: teddyvj (
teddy.vj@gmail.com)Severity: High / Critical (Mathematical Protocol Breakdown, Invariant Violation, Broken Compilation)
1. Summary of Vulnerabilities
A. Double-Scaling Score Deflation (Permanent Grade 'D' Protocol Collapse)
In
compute_risk_score:The sub-scores were partitioned as if unweighted (max 250 each: 250 * 4 = 1000). However, they were then multiplied by basis-point weights summing to 10,000 (100%) and divided by
BPS_DIVISOR(10,000).This effectively divided the scores by 4 twice. The maximum possible
weightedscore achievable by any pool—even with zero volatility, zero oracle deviation, and zero liquidations—was:(250 * 3000 + 250 * 2500 + 250 * 2500 + 250 * 2000) / 10000 = 250
In
score_to_letter_grade:Because the maximum score was capped at 250, any score >= 550 was mathematically impossible. Every single pool in the protocol was permanently evaluated as 'D' grade, regardless of safety or capital adequacy. Lending risk checks consuming this score would trigger false insolvency or refuse borrowing protocol-wide.
B. Compilation Failure via Invalid String Constructor
In
score_to_letter_grade:The contract attempted to construct a Soroban
Stringby passing an empty slice reference&[]as&Envand raw bytes as a string slice, failing compilation in upstreammain.C. Pool Mismatch in
record_pool_risk_scorerecord_pool_risk_score(env, pool, score)storedscoreunderDataKey::PoolRiskScore(pool)without verifying thatscore.pool == pool, allowing inconsistent cross-pool score records.D. Missing Authorization on
initializeinitialize(env, admin)lackedadmin.require_auth(), exposing the contract to front-running during initial deployment.2. Remediation
String::from_str(&env, LETTER_GRADES[idx]).assert_eq!(score.pool, pool, "pool address mismatch")inrecord_pool_risk_score.admin.require_auth()ininitialize."lib"tocrate-typeinCargo.tomlto support standard cargo testing and integration.3. Verification
All 11 unit tests pass with zero warnings:
4. Bounty Payout Information
In accordance with the repository's
SECURITY.mdBug Bounty Program:0xf5fcb1f90f8a2e658f38f72f0156ecbec7aa964dECYrMmKpVNyvWMuLNkBuWxqkk2TNRC3qVPDgonBwamKPbc1qjg5lug59rn9rz2j2f9ut798g99mn2asw9jfv0s