Stratax Contracts

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

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

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.

Updates

Lead Judging Commences

izuman Lead Judge 16 days ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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

Give us feedback!