Stratax Contracts

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

recoverTokens Can Drain Aave aToken Collateral

Author Revealed upon completion

Root + Impact

Location: src/Stratax.sol:282-284

Description

recoverTokens places no restriction on which token address is passed. Aave aTokens (e.g. aUSDC, aWETH) are standard ERC20 tokens held by the contract representing deposited collateral. Transferring them out reduces the collateral backing active positions without going through unwindPosition, bypassing health factor checks and flash loan repayment logic.

// src/Stratax.sol:282-284
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).transfer(owner, _amount); // @> _token = aUSDC silently drains Aave collateral
}

Risk

Likelihood:

  • Becomes exploitable when the contract evolves to hold positions for multiple users, as indicated by the NFT TODO comment at Stratax.sol:158

  • A compromised owner key can drain all aToken collateral in a single transaction

Impact:

  • Collateral drained without triggering any health factor or unwind checks — positions become immediately undercollateralized

  • In the planned multi-user NFT model, one user's collateral can be stolen, causing other users' positions to be liquidated

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {Stratax} from "../../src/Stratax.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
contract RecoverATokenPoCTest is Test {
Stratax stratax;
address aUSDC = 0xBcca60bB61934080951369a648Fb03DF4F96263C; // Aave v2 aUSDC mainnet
function test_ownerDrainsCollateralViaRecoverTokens() public {
address owner = stratax.owner();
uint256 aUSDCBalance = IERC20(aUSDC).balanceOf(address(stratax));
uint256 ownerBalanceBefore = IERC20(aUSDC).balanceOf(owner);
vm.prank(owner);
// @> aUSDC is a valid ERC20 — recoverTokens accepts it without restriction
stratax.recoverTokens(aUSDC, aUSDCBalance);
// All collateral drained from Aave positions in one tx
assertEq(IERC20(aUSDC).balanceOf(address(stratax)), 0);
assertEq(IERC20(aUSDC).balanceOf(owner), ownerBalanceBefore + aUSDCBalance);
// All positions backed by this collateral are now undercollateralized
}
}

Recommended Mitigation

Maintain a mapping of protected token addresses (Aave aTokens) that cannot be withdrawn via recoverTokens. The owner registers aToken addresses when positions are opened. This preserves the recovery function for genuinely stuck tokens (e.g. accidentally sent ERC20s) while preventing it from being used to drain active position collateral.

+ mapping(address => bool) public isProtectedToken;
+ function setProtectedToken(address _token, bool _status) external onlyOwner {
+ isProtectedToken[_token] = _status;
+ }
function recoverTokens(address _token, uint256 _amount) external onlyOwner {
+ require(!isProtectedToken[_token], "Cannot recover protected token");
IERC20(_token).transfer(owner, _amount);
}

Support

FAQs

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

Give us feedback!