Stratax Contracts

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

`recoverTokens` Can Drain Aave Collateral Tokens (aTokens)

Author Revealed upon completion

The `recoverTokens()` function has no restriction on which tokens can be recovered, allowing the owner to transfer Aave aTokens (which represent deposited collateral) out of the contract, degrading the health factor of all open positions.

Description

  • The `recoverTokens()` function is designed as an emergency mechanism to recover tokens accidentally sent to the contract. However, it accepts any ERC20 token address without validating whether the token is critical to the protocol’s operation.

  • The `recoverTokens()` function is designed as an emergency mechanism to recover tokens accidentally sent to the contract. However, it accepts any ERC20 token address without validating whether the token is critical to the protocol’s operation.

function recoverTokens(address _token, uint256 _amount) external onlyOwner {
IERC20(_token).transfer(owner, _amount); // @> No validation — accepts aTokens, USDC, WETH, anything
}

Note: Aave’s `finalizeTransfer` hook prevents transferring aTokens if it would make the position unhealthy (health factor < 1). However, the owner can still drain collateral up to the point just before liquidation, significantly degrading the health factor.

Risk

Likelihood: Low

  • The owner has unrestricted access to this function at any time, but the owner is a trusted role. Exploitation requires a malicious or compromised owner.

Impact: Medium

  • Funds are indirectly at risk. The owner can extract collateral value from the protocol by transferring aTokens to themselves, reducing the health factor of all positions and bringing them closer to liquidation.

  • Aave’s `finalizeTransfer` hook prevents draining all aTokens (blocks transfers that would make health factor -vvv

// STEP 1: Open a position
vm.startPrank(ownerTrader);
IERC20(WETH).transfer(address(stratax), 10 ether);
vm.stopPrank();
vm.startPrank(address(stratax));
IERC20(WETH).approve(AAVE_POOL, 10 ether);
IPoolMinimal(AAVE_POOL).supply(WETH, 10 ether, address(stratax), 0);
IPoolMinimal(AAVE_POOL).borrow(USDC, 5000e6, 2, 0, address(stratax));
vm.stopPrank();
uint256 hfBefore = _getHealthFactor();
// STEP 2: Owner drains 3 ETH worth of aTokens
vm.prank(ownerTrader);
stratax.recoverTokens(AAVE_AWETH, 3 ether);
uint256 hfAfter = _getHealthFactor();
uint256 ownerATokens = IERC20(AAVE_AWETH).balanceOf(ownerTrader);
// Owner received the aTokens
assertTrue(ownerATokens > 0, "Owner received aTokens");
// Health factor dropped significantly
assertTrue(hfAfter < hfBefore, "Health factor degraded");
// HF before: 3248300755888348940 (3.24)
// HF after: 2273810529121346243 (2.27)
// ~30% health factor reduction from draining 3 ETH of collateral
}

Recommended Mitigation

Add a blacklist of protected tokens that cannot be recovered:

function recoverTokens(address _token, uint256 _amount) external onlyOwner {
+ require(!isProtectedToken(_token), "Cannot recover protocol tokens");
IERC20(_token).transfer(owner, _amount);
}
+ function isProtectedToken(address _token) internal view returns (bool) {
+ // Check if token is an Aave aToken or variable debt token for any active reserve
+ (address aToken,,) = aaveDataProvider.getReserveTokensAddresses(_token);
+ if (aToken != address(0)) return true;
+ // Also block the underlying tokens used in active positions
+ return false;
+ }

Support

FAQs

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

Give us feedback!