Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Integer Division Truncation in Param Calculations Causes Systematic Under/Over-Estimation (Precision Loss → Unsafe/Failed Positions)

Integer Division Truncation in Param Calculations Causes Systematic Under/Over-Estimation (Precision Loss → Unsafe/Failed Positions)

Description:
Stratax::calculateOpenParams performs multiple chained arithmetic operations that rely on integer division. In Solidity, division truncates toward zero, so every / ... step can permanently discard precision.

This function computes critical values (flashLoanAmount, totalCollateralValueUSD, borrowValueUSD, borrowAmount, borrowValueInCollateral, flashLoanFee) using several mul / div patterns:

function calculateOpenParams(TradeDetails memory details)
public
view
returns (uint256 flashLoanAmount, uint256 borrowAmount)
{
...
@> flashLoanAmount =
(details.collateralAmount * (details.desiredLeverage - LEVERAGE_PRECISION)) / LEVERAGE_PRECISION;
...
@> uint256 totalCollateralValueUSD =
(totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
@> uint256 borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
@> borrowAmount = (borrowValueUSD * (10 ** details.borrowTokenDec)) / details.borrowTokenPrice;
@> uint256 flashLoanFee = (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
uint256 minRequiredAfterSwap = flashLoanAmount + flashLoanFee;
@> uint256 borrowValueInCollateral = (borrowAmount * details.borrowTokenPrice * (10 ** details.collateralTokenDec))
/ (details.collateralTokenPrice * (10 ** details.borrowTokenDec));
...
}

Even if each line looks correct, the cumulative truncation can cause borrowAmount (and the repayment feasibility check) to deviate from the intended economic model.

Impact:
Medium.

Precision loss here can lead to incorrect position sizing, with outcomes such as:

  • Under-borrowing → swap proceeds fail to cover flash loan + fee → revert (Insufficient borrow to repay flash loan) causing avoidable failures/DoS for otherwise valid inputs.

  • Over-borrowing (depending on rounding direction and where truncation happens) → larger debt than intended → higher liquidation risk / worse health factor than expected.

  • Inconsistent results across tokens with different decimals (6 vs 18) and low-priced assets, where truncation effects are magnified.

Because this function determines core leverage parameters, rounding errors directly affect safety and user experience.

Proof of Concept:
A representative failure pattern:

  • totalCollateralValueUSD is truncated down due to / 10^collateralDec

  • borrowValueUSD then truncates again after multiplying by ltv and safety margin

  • borrowAmount truncates again when dividing by borrowTokenPrice

  • Final check compares borrowValueInCollateral (also truncated) against minRequiredAfterSwap

With realistic inputs (e.g., 6-dec collateral, prices with 8 decimals, and non-integer conversion ratios), it’s possible for the math to land just below minRequiredAfterSwap due purely to rounding, even when the “true” real-number calculation would satisfy it.

Recommended Mitigation:
Use a consistent fixed-point strategy and aggregate multiplications before divisions, using mulDiv to preserve precision and control rounding.

A practical approach is OpenZeppelin’s Math.mulDiv (full precision) and explicitly choosing rounding direction where safety-critical:

  • Round up when computing amounts that must be sufficient to repay (borrowAmount, flashLoanFee, minRequiredAfterSwap)

  • Round down when enforcing caps (e.g., max borrow under LTV)

Example refactor pattern:

+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
// totalCollateralValueUSD = (totalCollateral * collateralPrice) / (10^collateralDec)
- uint256 totalCollateralValueUSD =
- (totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
+ uint256 totalCollateralValueUSD = Math.mulDiv(
+ totalCollateral,
+ details.collateralTokenPrice,
+ 10 ** details.collateralTokenDec
+ );
// borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000)
- uint256 borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
+ uint256 borrowValueUSD = Math.mulDiv(
+ totalCollateralValueUSD,
+ ltv * BORROW_SAFETY_MARGIN,
+ LTV_PRECISION * 10000
+ );
// borrowAmount = (borrowValueUSD * 10^borrowDec) / borrowTokenPrice
- borrowAmount = (borrowValueUSD * (10 ** details.borrowTokenDec)) / details.borrowTokenPrice;
+ borrowAmount = Math.mulDiv(
+ borrowValueUSD,
+ 10 ** details.borrowTokenDec,
+ details.borrowTokenPrice
+ );

For fee and “must-cover” computations, prefer rounding up:

- uint256 flashLoanFee = (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
+ uint256 flashLoanFee = Math.mulDiv(
+ flashLoanAmount,
+ flashLoanFeeBps,
+ FLASHLOAN_FEE_PREC,
+ Math.Rounding.Ceil
+ );

And similarly for borrowValueInCollateral, use mulDiv (possibly with Ceil if used as a sufficiency check).

This keeps economics stable, reduces false reverts, and makes leverage sizing safer across token decimal combinations.

Updates

Lead Judging Commences

izuman Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Precision Loss in Stratax::calculateOpenParams and unwindParams

Multiplication is done before division so precision loss is negligible

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!