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);
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));
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:
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
A user calls calculateOpenParams with USDC as collateral (6 decimals) but mistakenly passes collateralTokenDec = 18
The totalCollateralValueUSD is divided by 10^18 instead of 10^6, understating it by a factor of 10^12
The resulting borrowAmount is 10^12 times too small
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;
(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);
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.