Skip to content

[BUG BOUNTY] Risk-Scoring: Fatal Mathematical Score Deflation, Compilation Failure, and Pool Mismatch in Record #969

Description

@littfed

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

  1. 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).
  2. Correct String Construction: Migrated to String::from_str(&env, LETTER_GRADES[idx]).
  3. Pool Invariant Enforcement: Added assert_eq!(score.pool, pool, "pool address mismatch") in record_pool_risk_score.
  4. Initialization Protection: Added admin.require_auth() in initialize.
  5. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions