Stratax Contracts

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

[H-02] Open/Unwind DoS and Increase Liquidation Risk

Author Revealed upon completion

Root + Impact

### Description
The protocol performs multiple raw `IERC20.approve()` calls without checking the return value and without using SafeERC20. If a supported token returns false instead of reverting on failure (valid per ERC20 in practice), the protocol will assume approval succeeded and continue execution. Because all core flows (open and unwind) execute inside an Aave flash loan callback, a failed approval will cause downstream calls (supply, repay, swap) to revert, resulting in:
* Inability to open positions
* Inability to unwind positions
* Flash loan transaction reverts
* Increased liquidation risk if unwind fails
This creates a denial-of-service vector and reliability risk rooted in unsafe ERC20 handling.
### Affected Code
In `_executeOpenOperation`:
```solidity
function _executeOpenOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, FlashLoanParams memory flashParams) =
abi.decode(_params, (OperationType, address, FlashLoanParams));
// Step 1: Supply flash loan amount + user's extra amount to Aave as collateral
uint256 totalCollateral = _amount + flashParams.collateralAmount;
@> IERC20(_asset).approve(address(aavePool), totalCollateral);
aavePool.supply(_asset, totalCollateral, address(this), 0);
// Store initial balance to verify all borrowed tokens are used in swap
uint256 prevBorrowTokenBalance = IERC20(flashParams.borrowToken).balanceOf(address(this));
// Step 2: Borrow against the supplied collateral
aavePool.borrow(
flashParams.borrowToken,
flashParams.borrowAmount,
2, // Variable interest rate mode
0,
address(this)
);
//.....rest of the code which still contains other approve() calls
}
```
In `_executeUnwindOperation`:
```solidity
function _executeUnwindOperation(address _asset, uint256 _amount, uint256 _premium, bytes calldata _params)
internal
returns (bool)
{
(, address user, UnwindParams memory unwindParams) = abi.decode(_params, (OperationType, address, UnwindParams));
// Step 1: Repay the Aave debt using flash loaned tokens
@> IERC20(_asset).approve(address(aavePool), _amount);
aavePool.repay(_asset, _amount, 2, address(this));
//...rest of the function wich includes other approve() calls
}
```
No return value is validated.
### Impact
If approve() silently fails:
1. _executeOpenOperation DoS
* `aavePool.supply()` will revert due to missing allowance.
* Flash loan execution reverts.
* User cannot open new positions.
2. _executeUnwindOperation DoS
* `aavePool.repay()` or router swap will revert.
* Position cannot be unwound.
* User becomes exposed to liquidation risk.
3. Increased Liquidation Risk
If unwind fails during volatile conditions, debt remains open, collateral may be liquidated, and user cannot safely exit. Because operations are atomic inside `executeOperation()`, any failure bricks the entire action. This is especially critical for unwind operations.
### Risk
The protocol uses `IERC20(token).approve(spender, amount);` Without:
* Checking the return value
* Using SafeERC20
* Zero-reset allowance pattern
* Safe allowance increase pattern
The ERC20 standard does not strictly require approve() to revert on failure. Some tokens:
* Return false
* Require allowance reset to zero first (e.g., USDT-style behavior)
* Have non-standard return semantics
In such cases, the contract assumes approval succeeded when it did not.
### Recommended Mitigation
Use OpenZeppelin SafeERC20:
```solidity
using SafeERC20 for IERC20;
IERC20(token).safeApprove(spender, amount);
```
Or preferably:
```solidity
IERC20(token).safeIncreaseAllowance(spender, amount);
```
Better still, whitelist accepted ERC20 tokens.
### Proof of Concept
Create a file `test/unit/StrataxDoSPoC.t.sol` and the following:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Stratax} from "src/Stratax.sol";
contract AaveV3LeverageVulnerabilityTest is Test {
Stratax str;
// Ethereum Mainnet Addresses
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
address constant AAVE_POOL = 0x87870b2de3f17d34353663305e0102141f645E5d;
string ETH_RPC_URL = vm.envString("ETH_RPC_URL");
function setUp() public {
vm.createSelectFork(ETH_RPC_URL);
str = new Stratax();
}
function test_ExecuteOperation_RevertsOnUSDT() public {
Stratax.FlashLoanParams memory flashParams = Stratax.FlashLoanParams({
collateralToken: USDT,
collateralAmount: 1000 * 1e6,
borrowToken: address(0),
borrowAmount: 500 * 1e6,
oneInchSwapData: "",
minReturnAmount: 0
});
bytes memory params = abi.encode(
0, // OperationType.OPEN
address(this),
flashParams
);
// Simulate Aave calling the callback
vm.prank(AAVE_POOL);
// This will REVERT because USDT.approve returns nothing,
// while the contract expects a boolean.
vm.expectRevert();
str.executeOperation(USDT, 1000 * 1e6, 0, address(str), params);
}
}
```
Then run: forge test --match-test test_ExecuteOperation_RevertsOnUSDT -vv --fork-url $ETH_RPC_URL
NB; Add a RPC_URL before running this test.

Support

FAQs

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

Give us feedback!