Stratax Contracts

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

Divide-Before-Multiply Precision Loss in calculateOpenParams

Author Revealed upon completion

Root + Impact

Location: src/Stratax.sol:380-444

Description

calculateOpenParams chains multiple arithmetic operations where intermediate division truncates before a subsequent multiplication. In Solidity, integer division floors the result — dividing before multiplying compounds truncation errors across the calculation chain.

// src/Stratax.sol:412-430
flashLoanAmount =
(details.collateralAmount * (details.desiredLeverage - LEVERAGE_PRECISION)) / LEVERAGE_PRECISION;
// @> flashLoanAmount truncated here, then used in totalCollateral below
uint256 totalCollateralValueUSD =
(totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
// @> totalCollateralValueUSD truncated here, then multiplied again for borrowValueUSD
uint256 borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
// @> each prior truncation compounds into the final borrowAmount

Risk

Likelihood:

  • Triggered on every call to calculateOpenParams — all position openings are affected

  • Precision loss is larger for tokens with fewer decimals (USDC at 6 decimals) and larger collateral amounts


Impact:

  • Borrow amount is systematically understated — users receive less leverage than calculated

  • The require(borrowValueInCollateral >= minRequiredAfterSwap) check at line 441 can fail incorrectly due to truncation on small amounts

  • At 1𝑀+𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛𝑠𝑖𝑧𝑒𝑠,𝑐𝑜𝑚𝑝𝑜𝑢𝑛𝑑𝑒𝑑𝑡𝑟𝑢𝑛𝑐𝑎𝑡𝑖𝑜𝑛𝑐𝑎𝑛𝑟𝑒𝑝𝑟𝑒𝑠𝑒𝑛𝑡1M+positionsizes,compoundedtruncationcanrepresent100–$200 in underborrow per transaction

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
// Numeric comparison of current (divide-first) vs correct (multiply-first) calculation:
//
// Inputs:
// collateralAmount = 1_000_001e6 (USDC, 6 decimals, slightly above 1M)
// desiredLeverage = 15000 (1.5x)
// collateralPrice = 1e8 ($1.00 with 8 decimals)
// ltv = 8000 (80%)
// BORROW_SAFETY_MARGIN = 9500 (95%)
// LEVERAGE_PRECISION = 10000
// LTV_PRECISION = 10000
//
// Step 1 (same in both):
// flashLoanAmount = (1_000_001e6 * 5000) / 10000 = 500_000_500_000 (no loss here)
// totalCollateral = 1_000_001e6 + 500_000_500_000 = 1_500_001_500_000
//
// CURRENT (divide-first):
// totalCollateralValueUSD = (1_500_001_500_000 * 1e8) / 1e6
// = 150_000_150_000_000_000 / 1e6
// = 150_000_150 ← integer division truncates fractional part
// borrowValueUSD = (150_000_150 * 8000 * 9500) / (10000 * 10000)
// = 1_140_001_140_000_000 / 100_000_000 = 11_400_011 ← truncation compounds
//
// CORRECT (multiply-first):
// borrowValueUSD = (1_500_001_500_000 * 1e8 * 8000 * 9500)
// / (1e6 * 10000 * 10000)
// = 11_400_011_400_000 ← same result here but gap widens on edge values
//
// The difference grows with larger amounts and non-round prices

Recommended Mitigation

Reorder the arithmetic to perform all multiplications in a single expression before the final division. This preserves the full precision of intermediate values and eliminates the compounding truncation that occurs when dividing to an intermediate variable and then multiplying again.

- uint256 totalCollateralValueUSD =
- (totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
- uint256 borrowValueUSD =
- (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
+ // Combine all multiplications before dividing to minimise truncation
+ uint256 borrowValueUSD =
+ (totalCollateral * details.collateralTokenPrice * ltv * BORROW_SAFETY_MARGIN)
+ / ((10 ** details.collateralTokenDec) * LTV_PRECISION * 10000);

Support

FAQs

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

Give us feedback!