The LPNFT contract exhibits a desynchronization vulnerability where the router's internal records fail to update during certain NFT ownership transfers. This flaw allows unauthorized users (previous owners) to withdraw liquidity even after transferring their NFT on-chain. The vulnerability arises due to missing synchronization logic (e.g., router.afterUpdate) in the transfer process, leaving the router unaware of ownership changes.
The core issue lies in the absence of enforced synchronization between the NFT ownership (ownerOf) and the router's internal records (poolsFeeData). Specifically, when the transferFrom or equivalent transfer methods are used, the router is not notified of the ownership change unless explicitly programmed.
Root Cause:
The LPNFT contract relies on router.afterUpdate to synchronize NFT ownership changes with the router’s accounting.
However, the default transferFrom implementation in OpenZeppelin’s ERC721 does not include this synchronization.
Without explicitly overriding transferFrom to call router.afterUpdate, the router retains outdated ownership records.
Vulnerable Scenario:
Bob owns an NFT and transfers it to Alice on-chain (ownerOf reflects Alice as the new owner).
The router remains unaware of the transfer and continues to associate the NFT with Bob.
Bob exploits this desynchronization by withdrawing liquidity tied to the NFT, resulting in unauthorized fund access
Unauthorized Liquidity Withdrawals: The router believes the old owner still has the deposit, enabling a successful call to removeLiquidityProportional.
Desynchronization: On-chain ownership shows a new owner, but the router keeps referencing the old owner’s deposit data.
Potential Fund Loss: Once the old owner withdraws, the new on-chain owner effectively loses the liquidity they should control.
Add the following test to pkg/pool-hooks/test/foundry/UpliftExample.t.sol:
Result:
In the test logs, Bob initially deposits liquidity to obtain tokenId = 1. However, we then force a direct change in the NFT’s on-chain ownership to Alice by manipulating storage (bypassing _update). While Alice is the on-chain owner, the router still believes Bob holds the deposit (as shown by “Router sees Bob’s deposit count: 1”). Bob proceeds to withdraw successfully, meaning he effectively steals the funds, since the router has not been notified of the new on-chain owner (Alice).
Foundry: Used to simulate the desynchronization scenario using vm.store(...) to manipulate storage.
Manual Code Review: Identified the missing synchronization logic in the transfer process.
Override transferFrom and enforce synchronization:
Override transferFrom to include a call to router.afterUpdate(...) after the standard transfer logic:
Check On-Chain Ownership in Withdrawals
Verify ownerOf(tokenId) matches the caller in removeLiquidityProportional.
Use Hooks
Override _beforeTokenTransfer or _afterTokenTransfer to guarantee synchronization for every transfer route.
Reduce Redundant Bookkeeping
Reference ownerOf(tokenId) directly inside the router instead of relying on parallel deposit structures, minimizing desynchronization risks.
After adding the recommended mitigation function transferFrom to the LPNFT contract, include the following test in pkg/pool-hooks/test/foundry/UpliftExample.t.sol:
Result:
The logs confirm that, after Bob deposits and receives the NFT (step 1), he transfers the token to Alice using the mitigated logic (step 2). We then verify on-chain that Alice is now the owner (step 3) and that the router updates its records accordingly (step 4). The final outcome—Bob has a deposit count of 0 while Alice has 1—demonstrates there is no desynchronization and that the mitigation works as intended.
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.