Stratax Contracts

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

`Stratax::calculateOpenParams` trusts caller-supplied token decimals instead of querying the token contracts, enabling completely incorrect position parameters

Author Revealed upon completion

Stratax::calculateOpenParams trusts caller-supplied token decimals instead of querying the token contracts, enabling completely incorrect position parameters

Description

Stratax::calculateOpenParams accepts collateralTokenDec and borrowTokenDec as user-supplied fields in the TradeDetails struct, and uses them directly in every USD conversion and cross-token calculation without ever validating them against the actual token decimals (IERC20(token).decimals()):

struct TradeDetails {
address collateralToken;
address borrowToken;
uint256 desiredLeverage;
uint256 collateralAmount;
uint256 collateralTokenPrice;
uint256 borrowTokenPrice;
@> uint256 collateralTokenDec;
@> uint256 borrowTokenDec;
}

These unvalidated decimal values flow into three critical calculations:

1. Total collateral value in USD — wrong collateralTokenDec scales the entire position value incorrectly
2. Borrow amount conversion — wrong borrowTokenDec produces an incorrect borrow amount
3. Borrow-to-collateral conversion for flash loan repayment check — both wrong decimals compound, potentially allowing the sanity check to pass with completely wrong values

function calculateOpenParams(TradeDetails memory details)
public
view
returns (uint256 flashLoanAmount, uint256 borrowAmount)
{
// ...
@> uint256 totalCollateralValueUSD =
@> (totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
// Calculate borrow value in USD (with 8 decimals)
// Apply safety margin to ensure healthy position: borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000)
uint256 borrowValueUSD = (totalCollateralValueUSD * ltv * BORROW_SAFETY_MARGIN) / (LTV_PRECISION * 10000);
// Convert borrow value to borrow token amount
// borrowAmount = (borrowValueUSD * 10^borrowTokenDec) / borrowTokenPrice
@> borrowAmount = (borrowValueUSD * (10 ** details.borrowTokenDec)) / details.borrowTokenPrice;
// Ensure borrow amount when swapped back covers flash loan + fee
uint256 flashLoanFee = (flashLoanAmount * flashLoanFeeBps) / FLASHLOAN_FEE_PREC;
uint256 minRequiredAfterSwap = flashLoanAmount + flashLoanFee;
// Calculate the value of borrowed tokens in collateral token terms
// borrowValueInCollateral = (borrowAmount * borrowPrice * 10^collateralDec) / (collateralPrice * 10^borrowDec)
@> uint256 borrowValueInCollateral = (borrowAmount * details.borrowTokenPrice * (10 ** details.collateralTokenDec))
/ (details.collateralTokenPrice * (10 ** details.borrowTokenDec));
require(borrowValueInCollateral >= minRequiredAfterSwap, "Insufficient borrow to repay flash loan");
return (flashLoanAmount, borrowAmount);
}

This is in contrast to calculateUnwindParams and _executeUnwindOperation, which correctly query token decimals on-chain:

// In calculateUnwindParams — correctly queries decimals:
collateralToWithdraw = (debtTokenPrice * debtAmount * 10 ** IERC20(_collateralToken).decimals())
/ (collateralTokenPrice * 10 ** IERC20(_borrowToken).decimals());

Risk

Likelihood:

  • calculateOpenParams is a public function intended to be called by frontends or scripts to compute the flashLoanAmount and borrowAmount before calling createLeveragedPosition. An incorrect decimal value — whether from a frontend bug, user error, or a token with unusual decimals — will silently produce wrong results.

  • There are no guardrails: the function doesn't revert on wrong decimals, it simply returns wrong numbers. These wrong numbers are then passed directly to createLeveragedPosition, which executes the flash loan with them.

Impact:

  • If collateralTokenDec is too high (e.g., 18 instead of 6 for USDC): the collateral value is massively understated, producing a tiny borrowAmount. The flash loan repayment check may fail ("Insufficient borrow to repay flash loan"), or if it passes, the position is opened with far less debt than intended — wasting user capital on a position with effectively no leverage.

  • If collateralTokenDec is too low: the collateral value is massively overstated, producing an enormous borrowAmount. When this is passed to createLeveragedPosition, aavePool.borrow will revert because the position doesn't have enough collateral to support the borrow — but the flash loan has already been initiated, wasting gas.

  • If borrowTokenDec is wrong: similar cascading effects on the borrow amount and the flash loan repayment sanity check.

Proof of Concept

  1. A user calls calculateOpenParams with USDC as collateral (6 decimals) but mistakenly passes collateralTokenDec = 18

  2. The totalCollateralValueUSD is divided by 10^18 instead of 10^6, understating it by a factor of 10^12

  3. The resulting borrowAmount is 10^12 times too small

  4. The flash loan repayment check fails or the position is opened with negligible leverage

Add the following test to test/fork/Stratax.t.sol

function test_WrongDecimalsProducesWrongBorrowAmount() public {
console.log("");
console.log("============================================================");
console.log(" PoC: Unvalidated Token Decimals Produce Wrong Results");
console.log("============================================================");
uint256 collateralAmount = 1000 * 10 ** 6; // 1000 USDC
// Correct decimals: USDC = 6, WETH = 18
(uint256 correctFlashLoan, uint256 correctBorrow) = stratax.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: address(USDC),
borrowToken: address(WETH),
desiredLeverage: 30_000,
collateralAmount: collateralAmount,
collateralTokenPrice: 0,
borrowTokenPrice: 0,
collateralTokenDec: 6,
borrowTokenDec: 18
})
);
console.log("");
console.log(" [CORRECT] collateralTokenDec = 6, borrowTokenDec = 18");
console.log(" flashLoanAmount: ", correctFlashLoan);
console.log(" borrowAmount: ", correctBorrow);
// Wrong decimals: pass 18 for USDC instead of 6
// This will revert because the borrow is too small to repay flash loan
vm.expectRevert("Insufficient borrow to repay flash loan");
stratax.calculateOpenParams(
Stratax.TradeDetails({
collateralToken: address(USDC),
borrowToken: address(WETH),
desiredLeverage: 30_000,
collateralAmount: collateralAmount,
collateralTokenPrice: 0,
borrowTokenPrice: 0,
collateralTokenDec: 18,
borrowTokenDec: 18
})
);
console.log("");
console.log(" [WRONG] collateralTokenDec = 18 (should be 6)");
console.log(" Reverted: collateral value understated by 10^12");
console.log("");
console.log("============================================================");
console.log(" RESULT: Wrong decimals silently produce wrong parameters");
console.log(" or cause unexpected reverts");
console.log("============================================================");
console.log("");
}

Recommended Mitigation

Query the actual token decimals on-chain instead of trusting caller-supplied values. This matches what calculateUnwindParams and _executeUnwindOperation already do:

function calculateOpenParams(TradeDetails memory details)
public
view
returns (uint256 flashLoanAmount, uint256 borrowAmount)
{
+ uint256 collateralTokenDec = IERC20(details.collateralToken).decimals();
+ uint256 borrowTokenDec = IERC20(details.borrowToken).decimals();
+
// ...
uint256 totalCollateralValueUSD =
- (totalCollateral * details.collateralTokenPrice) / (10 ** details.collateralTokenDec);
+ (totalCollateral * details.collateralTokenPrice) / (10 ** collateralTokenDec);
// ...
- borrowAmount = (borrowValueUSD * (10 ** details.borrowTokenDec)) / details.borrowTokenPrice;
+ borrowAmount = (borrowValueUSD * (10 ** borrowTokenDec)) / details.borrowTokenPrice;
// ...
- uint256 borrowValueInCollateral = (borrowAmount * details.borrowTokenPrice * (10 ** details.collateralTokenDec))
- / (details.collateralTokenPrice * (10 ** details.borrowTokenDec));
+ uint256 borrowValueInCollateral = (borrowAmount * details.borrowTokenPrice * (10 ** collateralTokenDec))
+ / (details.collateralTokenPrice * (10 ** borrowTokenDec));
// ...
}

The collateralTokenDec and borrowTokenDec fields can then be removed from the TradeDetails struct entirely.

Support

FAQs

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

Give us feedback!