Flash Loans in Brief
Flash loans allow users to borrow assets without collateral, provided the loan is repaid within the same transaction. They're widely used in DeFi for arbitrage, collateral swaps, and liquidations.
The f(x) Protocol
The f(x) Protocol, a stablecoin system developed by AladdinDAO, allows users to mint the fxUSD stablecoin by creating collateralized debt positions (CDPs), each represented as a non-fungible token (NFT). While users can interact directly with the core contracts, most operations are routed through peripheral contracts, simplifying user interactions and enabling advanced features. These peripheral contracts are integrated with flash loan providers, allowing complex actions, such as unwinding positions, to be executed seamlessly within a single transaction.
Normal Execution Flow: Closing a Position
To understand how the protocol is typically used, and why certain deviations can lead to vulnerabilities, it's useful to walk through the standard flow for closing a position. This baseline behavior helps clarify the flaw discussed later.
This flow uses a peripheral contract, the f(x) router, that enables closing a position by initiating a flash loan in the same token as the position's collateral. This allows the contract to perform the entire close-and-repay cycle atomically: the loaned collateral is swapped into fxUSD to repay the debt, which unlocks the original collateral, and that unlocked collateral is then used to repay the flash loan.
Transaction 1 - Granting Approval
The user approves the router to transfer their NFT-based position so that it can later operate on the position on their behalf.
Transaction 2 - Closing the Position
1. The user initiates the position closure by calling closeOrRemovePositionFlashLoanV2 on the router.
2. The contract initiates a flash loan from Morpho (in the collateral token) and sets a HAS_FLASH_LOAN flag in the execution context.
3. Morpho calls back into the contracts' onMorphoFlashLoan, which checks the context flag and triggers onCloseOrRemovePositionFlashLoanV2.
4. This hook finalizes the operation:
- Swaps collateral into fxUSD to repay debt,
- Pulls the position NFT from the user,
- Interacts with the core contracts to close the position,
- Returns the NFT to the user.
5. The remaining collateral is optionally swapped and returned to the user.

The Exploit
The attacker exploits the protocol by back-running a victim’s approval transaction. By injecting a second flash loan during their position closure, the attacker reenters the contract and uses the victim’s approval to:
1. Close the victim’s position, and
2. Steal the collateral.
Below is a step-by-step breakdown of how this works.
Step 1: Create a Minimal Position
To interact with the router, the attacker first opens a position in the core system. For efficiency, they create a empty position, minimizing costs while setting the stage for the exploit.
Step 2: Trigger Flash Loan with a Malicious Payload
The attacker calls closeOrRemovePositionFlashLoanV2 on the router, targeting their own position. As part of the call, they include a payload with an empty swap path.
What's going on here?
The router typically swaps the flash-loaned token to fxUSD to repay debt, using a path defined by the user. However, if the swap is empty, the contract falls back to calling transferFrom on a token address supplied by the user. The attacker exploits this fallback by passing in a malicious contract instead of a legitimate token. When transferFrom is called, the malicious contract regains control over execution.
Step 3: First Flash Loan
The router initiates a flash loan from Morpho to cover the outstanding debt and enters the internal function onCloseOrRemovePositionFlashLoanV2. Everything here still looks normal, and the context is correct.
Step 4: Second Flash Loan via Malicious transferFrom
During the execution of the attacker's position closure, the router reaches the point where it tries to obtain fxUSD. Because the attacker-supplied an empty swap path, it defaults to calling transferFrom on the provided "token". That "token" is a malicious contract. When transferFrom is called, the attacker hijacks control flow and initiates a second flash loan, this time from Balancer, all within the execution context of the original (Morpho) loan.
Why is this dangerous?
The protocol uses a context flag (HAS_FLASH_LOAN) to track whether it's currently inside a flash loan execution. However, it isn't designed to handle nested flash loan callbacks. When the second (Balancer) flash loan arrives, the HAS_FLASH_LOAN flag is still set from the first one. As a result, the protocol mistakenly treats the reentrant call as a continuation of the original flow. This allows the attacker to reenter the system mid-execution with unvalidated calldata, now targeting the victim's position.
Step 5: Close Victim's Position and Steal Collateral
The protocol:
- Leverages the victim's recent approval to transfer their NFT.
- Closes the victim's position.
- Uses the flash-loaned funds to repay the victim's debt.
- Leaves the remaining collateral sitting in the contract.
- Finally, resumes execution the initial flashloan callback hook, which now sees the victim's collateral as part of their balance, and transfers it out as part of regular execution.

Important Caveat: Owner Checks Bypassed
In the intended execution flow, onCloseOrRemovePositionFlashLoanV2 is only ever called after a closeOrRemovePositionFlashLoanV2. In this context, the protocol fully controls the parameters, most importantly, it hardcodes msg.sender as the authorized user:
This ensures that only the caller's position can be modified.
However, in the exploit scenario, the attacker re-enters directly into onCloseOrRemovePositionFlashLoanV2 via an external flash loan callback, completely bypassing the expected flow. Crucially, the protocol still believes it is operating within the context of the original flash loan initiated via closeOrRemovePositionFlashLoanV2. This is because the internal context flag was never reset after the first flash loan completed. As a result, the check that should restrict flash loan callbacks is bypassed:
When the attacker triggers a second flash loan from Balancer, the vulnerable contract receives the callback (receiveFlashLoan) and, due to missing context validation, assumes the call is legitimate. It then executes the attacker-supplied payload, which includes arbitrary calldata, including a positionId belonging to another user.
Conclusion
This was a live vulnerability discovered during internal research by one of our auditors. The protocol assumed a single, controlled flash loan context, but failed to account for how nested or externally triggered flash loans could break those assumptions and compromise trust and control flow. By combining reentrancy, user-controlled calldata, and flash loan misdirection, an attacker could exploit this weakness to close another user's position and drain their collateral, all within a single transaction. Following responsible disclosure, the f(x) team promptly removed the Balancer V2 flash loan integration to eliminate this attack vector.
Key Lessons for Protocol Developers
User-Controlled Swap Routes Can Hijack Execution
Allowing users to define arbitrary swap paths introduces serious risk. In this case, it gave the attacker control over the execution to trigger a second flash loan and redirect execution back into the vulnerable contract with forged parameters.
Flash Loans Are Difficult to Reason About
Execution paths in flash loan scenarios are often non-linear and opaque. Without careful modeling, it's easy to overlook cases where trust boundaries are crossed implicitly through composable interactions.
Context Validation Must Be Robust
In this case, it wasn’t enough to check that the flash loan originated from a trusted protocol. The contract also needed to ensure that the flash loan was initiated by its logic and that the internal state wasn’t mistakenly inherited from a previous, unrelated flash loan. To enhance robustness, protocols should consider the following measures:
1. Hash and validate relevant context data (e.g. caller, owner, position ID) when executing flash loan callbacks, ensuring that the internal state matches the expected transaction flow.
2. Explicitly update or reset the flash loan context at the moment the flash loan provider calls back into the protocol. This avoids inheriting stale or unintended state from previous operations.
About us
ChainSecurity’s mission is to build trust within the blockchain ecosystem, to allow this emerging technology to reach its potential among established organizations, governments, and blockchain companies alike.
If you have questions, don’t hesitate to reach out to contact@chainsecurity.com for general requests including requests for audits, and for questions about this or other vulnerabilities. Also, visit us at chainsecurity.com.