Stratax Contracts

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

ERC20 transfer return values ignored

Author Revealed upon completion

Description

  • ERC‑20 interactions should be checked for success. Many tokens either (a) revert on failure, or (b) return false instead of reverting (e.g., USDT‑style). Robust contracts use OpenZeppelin’s SafeERC20 wrappers (which handle both behaviors) or explicitly require the boolean return value to be true.

  • Stratax calls transfer, transferFrom, and approve on ERC‑20s without verifying their return values and without SafeERC20. If a token returns false (no revert), the function continues as if the transfer/approval succeeded, causing downstream logic to execute with incorrect balances/allowances.

// Stratax.sol
// 1) User collateral pull — return value ignored
IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount); // @> no check
// 2) Approvals before Aave and 1inch — return values ignored
IERC20(_asset).approve(address(aavePool), totalCollateral); // @> no check
IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount); // @> no check
// 3) Recover tokens admin path — return value ignored (may silently fail)
IERC20(_token).transfer(owner, _amount); // @> no check

Risk

Likelihood: Medium

  • Non‑standard ERC20s that return false (not revert) are common; operationally, tokens like USDT require zero‑reset approvals and return false in some flows. These are routinely used as collateral/route assets.

  • Allowance patterns can fail silently if a token requires an approval reset to zero first; without checking the return value, the contract assumes success.

Impact: Medium

  • Operational DoS / late reverts: If transferFrom of user collateral returns false, the contract proceeds, but later Aave supply fails (insufficient balance), wasting gas and complicating incident triage.

  • Funds stuck / inconsistent state: recoverTokens may “succeed” while doing nothing, leaving assets stranded in the contract; failed approvals can leave swaps or repays under‑funded.

Proof of Concept

  • Conceptual pseudocode:

// Given: _flashLoanToken is a USDT-like token (returns false instead of reverting)
bool ok = IERC20(_flashLoanToken).transferFrom(user, address(this), collateralAmount);
// ok == false, but Stratax doesn't check it and continues.
// Stratax thinks it holds `collateralAmount` + flashLoan, approves/supplies to Aave.
// Aave pool pulls from Stratax, but Stratax only has the flashLoaned amount (no user collateral).
// Deep in Aave.supply: revert due to insufficient balance -> gas wasted; user confused.
// Similar silent-failure risks exist for approve(...) calls and recoverTokens(...).

Recommended Mitigation

  • Use SafeERC20 everywhere and follow allowance best practices. Replace direct calls with safeTransfer, safeTransferFrom, safeApprove/safeIncreaseAllowance, and reset allowances to zero when required.

- import {IERC20} from "forge-std/interfaces/IERC20.sol";
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
-contract Stratax is Initializable {
+contract Stratax is Initializable {
+ using SafeERC20 for IERC20;
// ...
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);
+ IERC20(_flashLoanToken).safeTransferFrom(msg.sender, address(this), _collateralAmount);
// ...
}
// In _executeOpenOperation / _executeUnwindOperation:
- IERC20(_asset).approve(address(aavePool), amount);
+ // Prefer setting to zero first if current allowance > 0 for USDT-like tokens
+ if (IERC20(_asset).allowance(address(this), address(aavePool)) > 0) {
+ IERC20(_asset).safeApprove(address(aavePool), 0);
+ }
+ IERC20(_asset).safeApprove(address(aavePool), amount);
- IERC20(flashParams.borrowToken).approve(address(oneInchRouter), flashParams.borrowAmount);
+ if (IERC20(flashParams.borrowToken).allowance(address(this), address(oneInchRouter)) > 0) {
+ IERC20(flashParams.borrowToken).safeApprove(address(oneInchRouter), 0);
+ }
+ IERC20(flashParams.borrowToken).safeApprove(address(oneInchRouter), flashParams.borrowAmount);
- IERC20(_token).transfer(owner, _amount); // recoverTokens
+ IERC20(_token).safeTransfer(owner, _amount);

Support

FAQs

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

Give us feedback!