Stratax Contracts

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

No validation of flash loan asset

Author Revealed upon completion

Description

  • When Aave calls executeOperation during a flash‑loan, the _asset provided by Aave must match the asset the protocol expects for the current operation:

    • OPEN: _asset should equal the collateral/flash‑loan token (collateralToken).

    • UNWIND: _asset should equal the debt token (debtToken).

    This invariant is critical because the logic that follows (supplying/withdrawing on Aave and swapping via 1inch) assumes a specific asset.

  • executeOperation decodes the OperationType and forwards to the internal handlers without asserting that the _asset passed by Aave matches the token encoded in the params. If _asset is wrong (due to misconfiguration, a malicious/mistaken pool address, or integration error), the contract can proceed with an asset it did not intend to handle - leading to failed swaps, incorrect supplies/repays, or unexpected token flows.

function executeOperation(
address _asset, uint256 _amount, uint256 _premium, address _initiator, bytes calldata _params
) external returns (bool) {
require(msg.sender == address(aavePool), "Caller must be Aave Pool");
require(_initiator == address(this), "Initiator must be this contract");
OperationType opType = abi.decode(_params, (OperationType));
if (opType == OperationType.OPEN) {
// @> No check: _asset should == flashParams.collateralToken
return _executeOpenOperation(_asset, _amount, _premium, _params);
} else {
// @> No check: _asset should == unwindParams.debtToken
return _executeUnwindOperation(_asset, _amount, _premium, _params);
}
}

Risk

Likelihood: Medium

  • Deployment / integration mistakes happen: wrong pool address, asset mis‑wiring, or params built for one asset while the pool sends another (especially across multi‑chain deployments).

  • Defense‑in‑depth gap: Even if Aave behaves correctly, this missing check leaves no guardrail if the pool address is accidentally pointed to a mock/wrong contract in staging or during upgrades—this will occur in practice over time.

Impact: Medium

  • Operational DoS / unexpected reverts: The 1inch calldata is prepared for a specific input/output pair. A mismatched _asset causes the swap to fail or return unexpected tokens, cascading into failed flash‑loan repayment.Impact 1

  • Incorrect accounting / token handling: The contract may approve/supply/repay the wrong token on Aave, leaving positions inconsistent or funds in unintended assets.

Proof of Concept

  • Pseudocode demonstrating the missing invariant check:

// SETUP (misconfiguration):
// - aavePool points to an attacker-controlled or wrong pool-like contract.
// - Caller (owner) triggers createLeveragedPosition expecting USDC as flash-loan asset.
// - Encoded params contain FlashLoanParams{ collateralToken = USDC, borrowToken = WETH, ... }.
// ATTACK / ERROR:
// The fake pool calls back executeOperation with:
// _asset = DAI (not USDC), _amount = X, _premium = Y,
// _params = abi.encode(OPEN, user, FlashLoanParams{collateralToken = USDC, ...})
// CURRENT BEHAVIOR:
// - executeOperation does NOT assert that _asset == collateralToken (USDC).
// - _executeOpenOperation proceeds, approving/supplying DAI, and attempting a WETH->USDC swap
// with calldata built for USDC destination, causing swap/repay logic to malfunction.
// EXPECTED DEFENSE:
// - require(_asset == collateralToken) for OPEN,
// - require(_asset == debtToken) for UNWIND,
// so the callback reverts immediately on mismatch.
  • A similar conceptual flow holds for UNWIND: _asset must be the debt token used to repay Aave; if a different asset is passed, the repay, withdraw, and swap sequence is inconsistent.

Recommended Mitigation

  • Add explicit assertions in executeOperation (or at the top of each internal handler) to ensure the Aave‐provided _asset matches the intended token in the encoded parameters.

function executeOperation(
address _asset,
uint256 _amount,
uint256 _premium,
address _initiator,
bytes calldata _params
) external returns (bool) {
require(msg.sender == address(aavePool), "Caller must be Aave Pool");
require(_initiator == address(this), "Initiator must be this contract");
- OperationType opType = abi.decode(_params, (OperationType));
- if (opType == OperationType.OPEN) {
- return _executeOpenOperation(_asset, _amount, _premium, _params);
- } else {
- return _executeUnwindOperation(_asset, _amount, _premium, _params);
- }
+ OperationType opType = abi.decode(_params, (OperationType));
+ if (opType == OperationType.OPEN) {
+ // Decode once to validate invariants
+ (, , FlashLoanParams memory fp) = abi.decode(_params, (OperationType, address, FlashLoanParams));
+ require(_asset == fp.collateralToken, "Flash-loan asset mismatch (OPEN)");
+ return _executeOpenOperation(_asset, _amount, _premium, _params);
+ } else {
+ (, , UnwindParams memory up) = abi.decode(_params, (OperationType, address, UnwindParams));
+ require(_asset == up.debtToken, "Flash-loan asset mismatch (UNWIND)");
+ return _executeUnwindOperation(_asset, _amount, _premium, _params);
+ }
}

Support

FAQs

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

Give us feedback!