Stratax Contracts

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

Slippage and Swap Execution Misconfiguration

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: Stratax uses 1inch to swap borrowed tokens to collateral during position opening and collateral to debt tokens during unwinding, with user-provided minReturnAmount for slippage protection.

  • Issue: The unwind flow applies a hardcoded 5% slippage buffer in calculateUnwindParams and ignores the user-provided minReturnAmount, while both createLeveragedPosition and unwindPosition are onlyOwner, so users cannot control slippage tolerance, leading to potential failed swaps and unrecoverable flash loans.// Root cause in the codebase with @> marks to highlight the relevant section

    // src/Stratax.sol
    /// @notice Calculates the amount of collateral to withdraw and debt to repay for unwinding a position
    @> function calculateUnwindParams(address _collateralToken, address _borrowToken)
    public
    view
    returns (uint256 collateralToWithdraw, uint256 debtAmount)
    {
    // ... calculations ...
    // Account for 5% slippage in swap
    @> collateralToWithdraw = (collateralToWithdraw * 1050) / 1000;
    return (collateralToWithdraw, debtAmount);
    }
    /// @notice Creates a leveraged position by taking a flash loan
    @> function createLeveragedPosition(
    address _flashLoanToken,
    uint256 _flashLoanAmount,
    uint256 _collateralAmount,
    address _borrowToken,
    uint256 _borrowAmount,
    bytes calldata _oneInchSwapData,
    uint256 _minReturnAmount
    ) public onlyOwner {
    // ... implementation ...
    }
    /// @notice Internal function to execute a token swap via 1inch
    @> function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
    internal
    returns (uint256 returnAmount)
    {
    // ... swap execution ...
    @> require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
    return returnAmount;
    }

Risk

Likelihood:

  • The owner controls all position operations and can set minReturnAmount arbitrarily low; unwind calculations use a fixed 5% buffer that may be insufficient during high volatility, causing swaps to return less than expected and fail the minReturnAmount check

Impact:

  • Failed swaps prevent flash loan repayment, causing the entire transaction to revert and leaving positions in an inconsistent state; owner-set minReturnAmount can be exploited to force failures or extract value.


Proof of Concept

  • Owner calls createLeveragedPosition with a very low minReturnAmount, allowing swaps to execute with excessive slippage

function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
  • During unwind, calculateUnwindParams adds only 5% to collateralToWithdraw regardless of market conditions

// Account for 5% slippage in swap
collateralToWithdraw = (collateralToWithdraw * 1050) / 1000;
  • If market volatility exceeds 5%, the 1inch swap returns less than minReturnAmount, causing _call1InchSwap to revert and the flash loan to fail repayment

    function _call1InchSwap(bytes memory _swapParams, address _asset, uint256 _minReturnAmount)
    internal
    returns (uint256 returnAmount)
    {
    // Execute the 1inch swap using low-level call with the calldata from the API
    (bool success, bytes memory result) = address(oneInchRouter).call(_swapParams);
    require(success, "1inch swap failed");
    // Decode the return amount from the swap
    if (result.length > 0) {
    (returnAmount,) = abi.decode(result, (uint256, uint256));
    } else {
    // If no return data, check balance
    returnAmount = IERC20(_asset).balanceOf(address(this));
    }
    // Sanity check
    require(returnAmount >= _minReturnAmount, "Insufficient return amount from swap");
    return returnAmount;
  • The unwind transaction reverts, leaving the position unchanged and potentially locking user funds.


Recommended Mitigation

  • Remove onlyOwner from createLeveragedPosition and unwindPosition so users control their own positions and slippage tolerance Stratax.sol:314-322 .

  • Add _slippageBps parameter to calculateUnwindParams to replace the hardcoded 5% buffer, defaulting to 5% when zero Stratax.sol:467-469 .

  • Add unwindPositionWithSlippage helper that includes a deadline check to prevent stale 1inch calldata execution.

  • The swap check in _call1InchSwap remains to enforce the user-provided minReturnAmount Stratax.sol:628-629 .

// src/Stratax.sol
- function createLeveragedPosition(
+ function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
- ) public onlyOwner {
+ ) public {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
// Transfer the user's collateral to the contract
IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
// ... rest unchanged ...
- function unwindPosition(
+ function unwindPosition(
address _collateralToken,
uint256 _collateralToWithdraw,
address _debtToken,
uint256 _debtAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
- ) external onlyOwner {
+ ) external {
UnwindParams memory params = UnwindParams({
collateralToken: _collateralToken,
collateralToWithdraw: _collateralToWithdraw,
debtToken: _debtToken,
debtAmount: _debtAmount,
oneInchSwapData: _oneInchSwapData,
minReturnAmount: _minReturnAmount
});
// ... rest unchanged ...
- function calculateUnwindParams(address _collateralToken, address _borrowToken)
+ function calculateUnwindParams(address _collateralToken, address _borrowToken, uint256 _slippageBps)
public
view
returns (uint256 collateralToWithdraw, uint256 debtAmount)
{
// ... existing calculations ...
- // Account for 5% slippage in swap
- collateralToWithdraw = (collateralToWithdraw * 1050) / 1000;
+ // Account for user-specified slippage (default 5% if _slippageBps == 0)
+ uint256 buffer = _slippageBps == 0 ? 1050 : (10000 + _slippageBps);
+ collateralToWithdraw = (collateralToWithdraw * buffer) / 1000;
return (collateralToWithdraw, debtAmount);
}
+ function unwindPositionWithSlippage(
+ address _collateralToken,
+ address _debtToken,
+ bytes calldata _oneInchSwapData,
+ uint256 _minReturnAmount,
+ uint256 _slippageBps,
+ uint256 _deadline
+ ) external {
+ require(block.timestamp <= _deadline, "Transaction expired");
+ (uint256 collateralToWithdraw, uint256 debtAmount) =
+ calculateUnwindParams(_collateralToken, _debtToken, _slippageBps);
+ this.unwindPosition(
+ _collateralToken,
+ collateralToWithdraw,
+ _debtToken,
+ debtAmount,
+ _oneInchSwapData,
+ _minReturnAmount
+ );
+ }

Support

FAQs

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

Give us feedback!