Stratax Contracts

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

Unwind Ignores User-Specified Collateral, Causing Swap Data Mismatch and DoS

Author Revealed upon completion

Root + Impact

Root Cause: _executeUnwindOperation ignores the user-provided _collateralToWithdraw and recalculates the withdrawal amount internally.
Impact: High. The swap calldata is generated for the user-requested amount, but the contract withdraws a different amount. This can cause swap failures and prevent unwinding positions, locking funds.

Description

  • Normal behavior: The unwind flow should use the user-provided collateral amount that the 1inch swap calldata was built for, or it should regenerate swap data for the actual withdrawn amount.

  • Issue: The function ignores unwindParams.collateralToWithdraw and computes a new collateralToWithdraw value, while still using the user’s oneInchSwapData. This mismatch can revert on insufficient balance or allowance in the swap.

// In _executeUnwindOperation
// @> user-provided collateralToWithdraw is ignored
uint256 collateralToWithdraw = (
_amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
withdrawnAmount = aavePool.withdraw(unwindParams.collateralToken, collateralToWithdraw, address(this));
// @> swap data is still user-provided and may target a different amount
uint256 returnAmount = _call1InchSwap(unwindParams.oneInchSwapData, _asset, unwindParams.minReturnAmount);

Risk

Likelihood:

  • Users build swap calldata off-chain for the amount they pass to unwindPosition.

Impact:

  • The unwind transaction can revert due to balance/allowance mismatch inside 1inch, trapping positions until prices move or the contract is upgraded.

Proof of Concept

  1. User requests a large collateral withdrawal and generates 1inch calldata for that amount.

  2. Contract ignores this amount and withdraws a smaller amount based on liquidation threshold.

  3. The swap tries to pull the larger amount and reverts.

// File: test/audit/PoC_UnwindSwapDataMismatch.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
contract MockERC20 {
string public name;
string public symbol;
uint8 public decimals;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
require(allowed >= amount, "allowance too low");
allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
contract MockAavePool {
MockERC20 public debtToken;
MockERC20 public collateralToken;
constructor(MockERC20 _debtToken, MockERC20 _collateralToken) {
debtToken = _debtToken;
collateralToken = _collateralToken;
}
function flashLoanSimple(address receiver, address asset, uint256 amount, bytes calldata params, uint16) external {
debtToken.transfer(receiver, amount);
Stratax(receiver).executeOperation(asset, amount, 0, receiver, params);
}
function repay(address, uint256 amount, uint256, address) external returns (uint256) {
debtToken.transferFrom(msg.sender, address(this), amount);
return amount;
}
function withdraw(address, uint256 amount, address to) external returns (uint256) {
collateralToken.transfer(to, amount);
return amount;
}
function supply(address, uint256, address, uint16) external {}
function borrow(address, uint256, uint256, uint16, address) external {}
function getUserAccountData(address)
external
pure
returns (uint256, uint256, uint256, uint256, uint256, uint256)
{
return (0, 0, 0, 0, 0, 2e18);
}
}
contract MockDataProvider {
uint256 public liqThreshold;
constructor(uint256 _liqThreshold) {
liqThreshold = _liqThreshold;
}
function getReserveConfigurationData(address)
external
view
returns (uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256)
{
return (0, liqThreshold, 0, 0, 0, 0, 0, 0, 0, 0);
}
function getReserveTokensAddresses(address)
external
view
returns (address, address, address)
{
return (address(0), address(0), address(0));
}
}
contract MockOracle {
mapping(address => uint256) public price;
function setPrice(address token, uint256 value) external {
price[token] = value;
}
function getPrice(address token) external view returns (uint256) {
return price[token];
}
}
contract Mock1InchRouter {
function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut)
external
returns (uint256 returnAmount, uint256 spentAmount)
{
MockERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
if (amountOut > 0) {
MockERC20(tokenOut).transfer(msg.sender, amountOut);
}
return (amountOut, amountIn);
}
}
contract PoC_UnwindSwapDataMismatch is Test {
Stratax public stratax;
MockERC20 public collateralToken;
MockERC20 public debtToken;
MockAavePool public aavePool;
MockDataProvider public dataProvider;
MockOracle public oracle;
Mock1InchRouter public oneInch;
address public ownerTrader = address(0x123);
function setUp() public {
collateralToken = new MockERC20("COLL", "COLL", 18);
debtToken = new MockERC20("DEBT", "DEBT", 6);
aavePool = new MockAavePool(debtToken, collateralToken);
dataProvider = new MockDataProvider(9500);
oracle = new MockOracle();
oneInch = new Mock1InchRouter();
oracle.setPrice(address(collateralToken), 1e8);
oracle.setPrice(address(debtToken), 1e8);
collateralToken.mint(address(aavePool), 2000 * 1e18);
debtToken.mint(address(aavePool), 2000 * 1e6);
Stratax impl = new Stratax();
UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this));
bytes memory initData = abi.encodeWithSelector(
Stratax.initialize.selector,
address(aavePool),
address(dataProvider),
address(oneInch),
address(debtToken),
address(oracle)
);
BeaconProxy proxy = new BeaconProxy(address(beacon), initData);
stratax = Stratax(address(proxy));
stratax.transferOwnership(ownerTrader);
}
function test_UnwindReverts_WhenSwapDataAmountExceedsWithdrawnCollateral() public {
uint256 debtToRepay = 1000 * 1e6;
uint256 userRequestedCollateral = 2000 * 1e18;
bytes memory swapData =
abi.encodeWithSelector(Mock1InchRouter.swap.selector, address(collateralToken), address(debtToken), userRequestedCollateral, debtToRepay);
vm.startPrank(ownerTrader);
vm.expectRevert();
stratax.unwindPosition(
address(collateralToken),
userRequestedCollateral,
address(debtToken),
debtToRepay,
swapData,
0
);
vm.stopPrank();
}
}

Test Result

forge test --match-path test/audit/PoC_UnwindSwapDataMismatch.t.sol -vv
Ran 1 test for test/audit/PoC_UnwindSwapDataMismatch.t.sol:PoC_UnwindSwapDataMismatch
[PASS] test_UnwindReverts_WhenSwapDataAmountExceedsWithdrawnCollateral() (gas: 143803)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.72ms (340.72µs CPU time)

Recommended Mitigation

Use the user-provided _collateralToWithdraw consistently, or rebuild the 1inch swap data based on the computed withdrawal amount.

- uint256 collateralToWithdraw = (
- _amount * debtTokenPrice * (10 ** IERC20(unwindParams.collateralToken).decimals()) * LTV_PRECISION
- ) / (collateralTokenPrice * (10 ** IERC20(_asset).decimals()) * liqThreshold);
+ uint256 collateralToWithdraw = unwindParams.collateralToWithdraw;

Support

FAQs

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

Give us feedback!