Stratax Contracts

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

Unchecked ERC20 Return Values Lead to Silent Failures and Fund Lockup

Author Revealed upon completion

Description:

  • The protocol relies on standard IERC20 transfer and transferFrom calls to move assets, expecting them to revert on failure to ensure state consistency

  • However, the return values of these calls are ignored. This leads to silent failures for tokens that return false (e.g., ZRX), allowing the protocol to proceed without receiving funds (potentially using its own idle funds), and causes permanent fund lockup for tokens that return void (e.g., USDT) as the calls revert due to interface mismatch.

/**
* @notice Emergency function to recover tokens sent to contract
* @param _token The token address to recover
* @param _amount The amount to recover
*/
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
// Vulnerability: Return value ignored.
// 1. If token returns false (fail), this succeeds silently.
// 2. If token returns void (USDT), this reverts due to ABI mismatch.
@> IERC20(_token).transfer(owner, _amount);
}
---------
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");
// Transfer the user's collateral to the contract
// Vulnerability: If transferFrom returns false (e.g. ZRX), execution proceeds
// without receiving funds, potentially using contract's idle funds.
@> IERC20(_flashLoanToken).transferFrom(msg.sender, address(this), _collateralAmount);
FlashLoanParams memory params = FlashLoanParams({
collateralToken: _flashLoanToken,

Risk

Likelihood:

  • Common Token Standard: USDT is the most widely used stablecoin in DeFi. The likelihood of a user or the protocol interacting with USDT is near 100%.

  • Standard Admin Function: recoverTokens is a safety feature intended to be used. Finding it broken for the most common asset is a significant failure.

  • Supported Assets: The protocol supports "All EVM-compatible chains with Aave V3", which includes tokens like ZRX and EURS that exhibit the "silent failure" behavior.

Impact:

  • Permanent Fund Lockup (USDT): The protocol explicitly claims to support "ERC20 tokens supported by Aave V3". USDT is the largest asset on Aave. Because USDT's transfer function does not return a boolean (it returns void), calling it via the standard IERC20 interface (which expects a bool) causes the transaction to revert due to a return data size mismatch. This means recoverTokens is completely broken for USDT. Any USDT sent to the contract (accidentally or otherwise) is permanently stuck.

  • Protocol Fund Theft / Free Leverage: For tokens that return false on failure (e.g., ZRX, EURS), the createLeveragedPosition function fails to pull user collateral but proceeds anyway. If the contract holds any idle funds of that asset (e.g., accumulated fees or unrecovered tokens), the attacker can use the protocol's own funds to open a leveraged position for themselves, effectively stealing the liquidity.

Proof of Concept:

function test_UncheckedReturn_USDT_StuckFunds() public {
// 1. Deploy a token that does NOT return a boolean (like USDT)
NoReturnToken usdt = new NoReturnToken();
// 2. Try to recover tokens
vm.startPrank(ownerTrader);
// This call REVERTS because Stratax expects a bool return value but gets nothing.
// This means any USDT sent to the contract is PERMANENTLY STUCK.
vm.expectRevert();
stratax.recoverTokens(address(usdt), 1000);
vm.stopPrank();
console2.log("recoverTokens() reverted for USDT-like token. Funds are stuck.");
}
function test_UncheckedReturn_CreatePosition_ProceedsOnFailure() public {
// 1. Deploy Faulty Token (returns false on transferFrom)
FaultyERC20 faultyToken = new FaultyERC20();
// 2. Mock Aave Pool to avoid complex callback logic
// We just want to prove execution reaches this point despite transferFrom failing
vm.mockCall(
AAVE_POOL,
abi.encodeWithSelector(IPool.flashLoanSimple.selector),
abi.encode()
);
vm.startPrank(ownerTrader);
// 3. Call createLeveragedPosition
// If the return value was checked, this would revert.
// Since it succeeds, it proves the contract ignored the failed transfer and proceeded.
stratax.createLeveragedPosition(address(faultyToken), 0, 100, USDC, 1000, "", 0);
vm.stopPrank();
console2.log("createLeveragedPosition proceeded despite transferFrom failure.");
}

Recommended Mitigation

- remove this code
+ add this code

Support

FAQs

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

Give us feedback!