Stratax Contracts

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

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

Author Revealed upon completion

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.

Support

FAQs

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

Give us feedback!