Stratax Contracts

First Flight #57
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

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

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;
Updates

Lead Judging Commences

izuman Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

unwind ignore collateralToWithdraw

function logic ignores user input

Appeal created

wojack0x0 Submitter
16 days ago
izuman Lead Judge
12 days ago
izuman Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

unwind ignore collateralToWithdraw

function logic ignores user input

Support

FAQs

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

Give us feedback!