Stratax Contracts

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

Integer Precision Loss in Leverage Calculations

Author Revealed upon completion

Root + Impact

Description

  • The calculateOpenParams function computes the flash loan amount and borrow amount needed to achieve a desired leverage. It performs multiple sequential integer divisions: first to compute the flash loan amount, then collateral value in USD, then borrow value in USD, then borrow amount in token units. Each division truncates the fractional remainder.

  • With tokens that have few decimals (e.g., USDC with 6, WBTC with 8, or exotic tokens with 2-4), the truncation from sequential integer divisions compounds. In the worst case, the flash loan fee calculation (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC can truncate to zero, effectively providing a fee-free flash loan.

function calculateOpenParams(TradeDetails memory details) public view returns (...) {
// Division 1: flash loan amount
@> flashLoanAmount = (details.collateralAmount * (details.desiredLeverage - LEVERAGE_PRECISION)) / LEVERAGE_PRECISION;
uint256 totalCollateral = details.collateralAmount + flashLoanAmount;
// Division 2: collateral value in USD
@> uint256 totalCollateralValueUSD = (totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
// Division 3: borrow value in USD
@> uint256 borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
// Division 4: borrow amount in tokens
@> borrowAmount = (borrowValueUSD * (10 ** details.borrowTokenDec)) / details.borrowTokenPrice;
// Division 5: flash loan fee
@> uint256 flashLoanFee = (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
}

Risk

Likelihood:

  • The protocol is designed to work with USDC (6 decimals) and potentially other low-decimal tokens. With small position sizes in low-decimal tokens, each division truncates a proportionally larger amount.

  • The flashLoanFeeBps is 9 (0.09%), meaning flash loan amounts below ~1111 raw units result in a fee of zero: (1111 * 9) / 10000 = 0.

Impact:

  • Compounding rounding errors across five sequential divisions can cause borrowValueInCollateral to fall below minRequiredAfterSwap, making certain leverage ratios and position sizes impossible to execute despite being mathematically valid.

  • The zero-fee flash loan edge case means Aave does not receive its protocol fee for small positions, and the minRequiredAfterSwap check becomes less strict than intended.

Proof of Concept

The following arithmetic walkthrough shows that for a token with only 2 decimals, the flash loan fee formula (flashLoanAmount * 9) / 10000 truncates to zero when the raw amount is less than 1112 units. This effectively grants a fee-free flash loan. Even with 6-decimal USDC, very small positions experience proportionally significant truncation across the five sequential divisions.

// Example with a 2-decimal token, collateralAmount = 100 (1.00 tokens):
//
// flashLoanAmount = (100 * (20000 - 10000)) / 10000 = 100
// flashLoanFee = (100 * 9) / 10000 = 0 <-- TRUNCATED TO ZERO
//
// With 6-decimal USDC, collateralAmount = 1000000 (1.00 USDC):
// flashLoanAmount = (1000000 * 10000) / 10000 = 1000000
// flashLoanFee = (1000000 * 9) / 10000 = 900 <-- $0.0009, correct
//
// With 2-decimal token, collateralAmount = 100 (1.00 tokens at $100):
// flashLoanFee = (100 * 9) / 10000 = 0 <-- FEE-FREE FLASH LOAN

Recommended Mitigation

Round the flash loan amount up instead of down so the fee calculation never truncates to zero, and add an explicit check that the flash loan fee is nonzero whenever a flash loan is taken. This prevents the fee-free edge case while preserving the existing calculation flow.

function calculateOpenParams(TradeDetails memory details) public view returns (...) {
// ...
+ // Use higher precision intermediate values to reduce truncation
+ uint256 PRECISION_FACTOR = 1e18;
+
flashLoanAmount =
- (details.collateralAmount * (details.desiredLeverage - LEVERAGE_PRECISION)) / LEVERAGE_PRECISION;
+ (details.collateralAmount * (details.desiredLeverage - LEVERAGE_PRECISION) + LEVERAGE_PRECISION - 1)
+ / LEVERAGE_PRECISION; // Round up
// ...
uint256 flashLoanFee = (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
+ require(flashLoanFee > 0 || flashLoanAmount == 0, "Flash loan fee underflow");
}

Support

FAQs

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

Give us feedback!