Stratax Contracts

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

Unchecked ERC20 `transfer` and `transferFrom` Return Values Allow Silent Failures During Flash Loan Operations

Author Revealed upon completion

Root + Impact

Description

  • The Stratax contract uses raw ERC20 transfer(), transferFrom(), and approve() calls without checking their boolean return values. Some widely-used ERC20 tokens (e.g., USDT) return false on failure instead of reverting. When these calls silently fail, the contract continues execution as if the operation succeeded.

  • Explain the specific issue or problem in one or more sentences

// Root cause in the codebase with @> marksThe two most critical instances are in `recoverTokens()` and `createLeveragedPosition()`:
```javascript
// src/Stratax.sol
// recoverTokens() — Line 283
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
@> IERC20(_token).transfer(owner, _amount); // Return value not checked
}
// createLeveragedPosition() — Line 325
function createLeveragedPosition(
address _flashLoanToken,
uint256 _flashLoanAmount,
uint256 _collateralAmount,
address _borrowToken,
uint256 _borrowAmount,
bytes calldata _oneInchSwapData,
uint256 _minReturnAmount
) public onlyOwner {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
@> IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount); // Return value not checked
// ... proceeds to flash loan with potentially zero actual collateral
}
```
Additionally, all `approve()` calls inside the flash loan callbacks are unchecked:
```javascript
// _executeOpenOperation() — Lines 495, 510, 530, 534
@> IERC20(_asset).approve(address(aavePool), totalCollateral);
@> IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
@> IERC20(_asset).approve(address(aavePool), returnAmount - totalDebt);
@> IERC20(_asset).approve(address(aavePool), totalDebt);
// _executeUnwindOperation() — Lines 559, 583, 593, 597
@> IERC20(_asset).approve(address(aavePool), _amount);
@> IERC20(unwindParams.collateralToken).approve(address(oneInchRouter), withdrawnAmount);
@> IERC20(_asset).approve(address(aavePool), returnAmount - totalDebt);
@> IERC20(_asset).approve(address(aavePool), totalDebt);

Risk

Likelihood:

  • The protocol operates on "Ethereum Mainnet" and "all EVM-compatible chains with Aave V3, 1inch, and Chainlink deployed," where tokens like USDT are among the most used assets

  • USDT is one of the most commonly used stablecoins in DeFi and does not return true on successful transfer/approve — it returns nothing, which is interpreted as false by standard ABI decoding

Impact:

  • In recoverTokens(): The owner believes tokens were recovered but they remain stuck in the contract permanently — loss of funds

  • In createLeveragedPosition(): If transferFrom silently fails, the contract proceeds to take a flash loan with zero actual user collateral, leading to an undercollateralized position or a confusing revert deeper in the flash loan callback

  • In flash loan callbacks: If any approve() silently fails, subsequent Aave supply/borrow/repay or 1inch swap calls will fail, potentially trapping funds mid-flash-loan since the flash loan must be repaid in the same transaction

Proof of Concept

// Scenario: Owner tries to recover USDT tokens stuck in contract
// 1. Some USDT is accidentally sent to or left in the Stratax contract
// 2. Owner calls recoverTokens()
stratax.recoverTokens(USDT_ADDRESS, 1000e6);
// 3. USDT.transfer() returns nothing (not true/false) — raw call "succeeds"
// but USDT requires the sender to have zero allowance before setting a new one
// OR the transfer could fail for other USDT-specific reasons
// 4. The function completes without revert
// 5. Owner believes tokens were recovered, but they remain in the contract
// Similar issue with createLeveragedPosition:
// If transferFrom silently fails, collateralAmount worth of tokens
// never arrives, but the flash loan proceeds anyway

Recommended Mitigation

+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract Stratax is Initializable {
+ using SafeERC20 for IERC20;
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
- IERC20(_token).transfer(owner, _amount);
+ IERC20(_token).safeTransfer(owner, _amount);
}
function createLeveragedPosition(...) public onlyOwner {
require(_collateralAmount > 0, "Collateral Cannot be Zero");
- IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
+ IERC20(_flashLoanToken).safeTransferFrom(msg.sender, address(this), _collateralAmount);
// ...
}
// Apply safeApprove (or forceApprove) to all approve() calls:
- IERC20(_asset).approve(address(aavePool), totalCollateral);
+ IERC20(_asset).forceApprove(address(aavePool), totalCollateral);
// ... repeat for all 8 approve instances
}

Support

FAQs

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

Give us feedback!