Stratax Contracts

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

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

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

Lead Judging Commences

izuman Lead Judge 16 days ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

WEIRD ERC20 Tokens

Currently there is no support for weird ERC20 tokens i.e. FOT tokens, missing return values, reentrancy etc.

Support

FAQs

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

Give us feedback!