Normal behavior: The protocol allows `desiredLeverage >= LEVERAGE_PRECISION` (1x). For 1x, no extra collateral from a flash loan is needed: the user supplies collateral C and the intended total collateral is C, so flash loan amount should be 0 and no borrow/swap should occur (supply-only position).
Specific issue: When `desiredLeverage == LEVERAGE_PRECISION` (1x), `calculateOpenParams` returns `flashLoanAmount == 0` but still returns a positive `borrowAmount`. The code never treats “zero flash loan” as a degenerate case. It always calls Aave’s `flashLoanSimple` with that amount and, in the callback, always performs supply, borrow, swap, and then supplies any swap output. So either (1) the pool reverts on a 0-amount flash loan and the user cannot open 1x, or (2) the pool allows it and the user receives a leveraged position (collateral = C + swap output, debt > 0) instead of a 1x supply-only position.
Likelihood:
The protocol’s public API allows `desiredLeverage == LEVERAGE_PRECISION` (1x). Any caller that uses `calculateOpenParams(..., desiredLeverage: 10000, ...)` and then `createLeveragedPosition(..., flashLoanAmount, ..., borrowAmount, ...)` with those return values will request a 0-size flash loan and a positive borrow. There is no code path that avoids this when 1x is selected.
Whether the outcome is revert or wrong leverage depends only on the external pool’s behavior (revert on 0 amount or not). In both cases the protocol’s behavior is wrong for the advertised 1x case.Impact:
If the pool reverts on 0-amount flash loan: Users attempting to open a 1x position suffer a revert (DoS). Gas is spent and no position is opened. The advertised “1x” option is unusable
If the pool allows 0-amount flash loan: The user receives a leveraged position (collateral > C, debt > 0) instead of a 1x supply-only position. They are exposed to borrowing and liquidation risk they did not intend. This can lead to unexpected liquidations and loss of funds when the user believed they had no leverage.
Test 1 — API returns (0, positive borrow) for 1x:
test_PoC_1x_calculateOpenParams_returns_zero_flash_loan_positive_borrow calls calculateOpenParams with desiredLeverage: LEVERAGE_PRECISION (1x) and asserts flashLoanAmount == 0 and borrowAmount > 0. So the protocol itself returns params that lead to a 0-size flash loan and a positive borrow.
Test 2 — Execution path would produce leverage > 1:
test_PoC_1x_execution_path_would_produce_leveraged_position_not_1x asserts the callback logic when _amount == 0: initial supply is only user collateral C, then the code borrows and swaps and supplies the full returnAmount (because totalDebt == 0). So final collateral = C + returnAmount and debt > 0, i.e. effective leverage > 1x.
E2E on fork: Call createLeveragedPosition(flashLoanToken, 0, collateralAmount, borrowToken, borrowAmount, swapData, minReturn) with the return values of calculateOpenParams(1x). If the pool reverts on 0 amount, the tx reverts (DoS). If it allows 0, the resulting position has collateral > user collateral and debt > 0 (wrong leverage).
Option A (simplest): Disallow 1x so the degenerate case is out of scope. In calculateOpenParams, require strictly greater than 1x:
Optionally add the same check in createLeveragedPosition when it is used with params from calculateOpenParams (e.g. require _flashLoanAmount > 0), and document that 1x is not supported.
Option B (support true 1x): Explicitly handle the case when no flash loan is needed. For example, in createLeveragedPosition, when _flashLoanAmount == 0: only pull user collateral, supply it to Aave, and return; do not call flashLoanSimple, and do not borrow or swap. That way 1x corresponds to a supply-only position with no leverage.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.