CVE-2026-24783

Broken Math on the Blockchain: Inside CVE-2026-24783

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 28, 2026·6 min read·3 visits

Executive Summary (TL;DR)

The `soroban-fixed-point-math` library for Stellar smart contracts failed to handle 'double negative' division correctly and lacked overflow checks when casting `i128` down to `i64`. This allows attackers to trigger incorrect rounding or wrap negative values into positive ones.

A critical logic error in the `soroban-fixed-point-math` library allows for incorrect rounding and integer overflows in signed arithmetic operations. This vulnerability affects Stellar Soroban smart contracts, potentially turning massive deficits into massive gains via narrowing cast errors.

The Hook: When 1 + 1 Equals Panic

In the unforgiving world of blockchain development, floating-point numbers are contraband. They are non-deterministic, messy, and banned from most virtual machines, including Stellar's Soroban. To perform financial math—calculating interest rates, slippage, or token shares—developers rely on fixed-point arithmetic libraries. These libraries are the bedrock of DeFi; if they crack, the whole building comes down.

Enter soroban-fixed-point-math, a popular Rust crate used to handle these precise calculations on the Stellar network. It promises safe, high-precision math for i64 and i128 types. But in versions 1.3.0 and 1.4.0, that promise was broken.

CVE-2026-24783 isn't just a rounding error; it's a fundamental misunderstanding of signed integers that could allow a smart contract to confuse a massive debt for a massive surplus. It represents the classic 'silent killer' of smart contracts: logic that looks right at a glance but falls apart under specific, mathematically valid edge cases.

The Flaw: Double Negatives and Truncated Bits

The vulnerability stems from two distinct but equally embarrassing failures in the library's mulDiv logic. The first is a failure of basic algebra. When performing division, the library needed to determine if the result was negative to apply the correct 'floor' or 'ceil' rounding logic. The developers wrote a check that looked essentially like this: if product < 0.

Here is the problem: If the product (numerator) is negative and the divisor is also negative, the result should be positive. However, the code saw the negative product, panicked, and applied negative-number rounding logic to a positive result. This leads to off-by-one errors that, while annoying, are rarely catastrophic on their own.

The second flaw is the main event. When performing calculations on 64-bit signed integers (i64), the library rightfully promotes them to 128-bit (i128) to prevent intermediate overflows. However, when casting the result back down to i64, the library only checked if the value was too big (> i64::MAX). It completely forgot to check if the value was too small (< i64::MIN).

Rust's as keyword performs a truncating cast. If you take a massive negative number in i128 (one that exceeds the lower bounds of i64) and blindly cast it as i64, you don't get an error. You get a wrap-around. The sign bit gets chopped or misinterpreted, and suddenly, a deeply negative value acts like a large positive one.

The Code: The Smoking Gun

Let's look at the Rust code responsible for the narrowing overflow. This is a simplified view of the i64 implementation before the patch. The goal is to return Option<i64>, where None indicates an overflow.

Vulnerable Code (Simplified):

fn mul_div(a: i64, b: i64, c: i64) -> Option<i64> {
    // Promote to i128 to avoid intermediate overflow
    let r = (a as i128) * (b as i128);
    let res_i128 = r / (c as i128);
 
    // THE BUG: Only checks upper bound!
    if res_i128 > (i64::MAX as i128) {
        return None;
    }
 
    // Blind cast. If res_i128 is smaller than i64::MIN,
    // this wraps around!
    Some(res_i128 as i64)
}

If res_i128 is -9,223,372,036,854,775,809 (just one below i64::MIN), the upper bound check passes (it's certainly not greater than MAX). The cast then strips the high bits, and the binary representation is reinterpreted within the i64 bit space, resulting in a completely different, positive number.

The Fix (Commit c9233f7):

fn mul_div(a: i64, b: i64, c: i64) -> Option<i64> {
    let r = (a as i128) * (b as i128);
    let res_i128 = r / (c as i128);
 
    // The Fix: Use TryFrom or check both bounds
    i64::try_from(res_i128).ok()
}

The fix is elegantly simple: stop doing manual bounds checks if you're bad at them. Rust's try_from handles all edge cases of narrowing casts automatically.

The Exploit: Turning Debt into Profit

How do we weaponize this? Imagine a DeFi lending protocol on Soroban. The protocol tracks your account health using signed integers: positive values mean you are solvent, negative values mean you are in debt. The protocol uses soroban-fixed-point-math to calculate the "Adjusted Health Factor" after a market crash.

The Scenario:

  1. The Setup: You have a position that is underwater. The protocol calculates your new balance using a formula like: (CurrentBalance * LeverageFactor) / MarketIndex.
  2. The Trigger: You manipulate the inputs (perhaps via a flash loan or by choosing a volatile asset pair) such that the numerator CurrentBalance * LeverageFactor results in a massive negative number, technically expressible in i128 but far below i64::MIN.
  3. The Execution: The contract calls mul_div. The intermediate result is, say, -2^64.
  4. The Magic: The vulnerable library checks: Is -2^64 greater than i64::MAX? No. It proceeds.
  5. The Prestige: The result is cast to i64. Due to two's complement behavior, specific bit patterns of large negative numbers can wrap around to zero or positive values.

Instead of the contract realizing you are hopelessly insolvent and liquidating you, the function returns a positive i64. Your dashboard suddenly shows you have a massive surplus. You withdraw the "excess" collateral and walk away, leaving the protocol with bad debt and a broken ledger.

The Fix: Back to Elementary School

The remediation for this vulnerability is straightforward but urgent. The developers of soroban-fixed-point-math released versions 1.3.1 and 1.4.1 to address the issue. The patch does two things:

  1. Corrects the Logic: It properly evaluates the sign of both operands in the division. The new logic checks (r < 0 && z > 0) || (r > 0 && z < 0) to determine if the result is truly negative before applying negative rounding.
  2. Safe Casting: It replaces the unsafe as i64 casting with i64::try_from(), which inherently checks both lower and upper bounds and returns an error if the value cannot fit in the target type.

If you are a developer using this library, you cannot simply "hope for the best." Signed integer bugs are notoriously difficult to fuzz because they reside at the extreme edges of the number line. Check your Cargo.toml file immediately.

> [!NOTE] > If your contract is immutable and deployed with the vulnerable version, you are in a tight spot. You will need to migrate state to a new contract or execute an administrative pause if your governance model supports it.

Fix Analysis (2)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
EPSS Probability
0.01%
Top 99% most exploited

Affected Systems

Soroban Smart ContractsStellar DeFi ProtocolsRust applications using soroban-fixed-point-math

Affected Versions Detail

Product
Affected Versions
Fixed Version
soroban-fixed-point-math
script3
1.3.01.3.1
soroban-fixed-point-math
script3
1.4.01.4.1
AttributeDetail
CWE IDCWE-682
Attack VectorNetwork
CVSS Score7.5 (High)
ImpactIntegrity Loss (Calculation Errors)
Exploit StatusPoC Available
LanguageRust
CWE-682
Incorrect Calculation

The software performs a calculation that generates incorrect or inaccurate results that can affect the control flow or data integrity of the system.

Vulnerability Timeline

Vulnerability identified and patched
2026-01-26
GHSA Advisory Published
2026-01-27
CVE Assigned
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.