# openPosition() use stale feeGrowthInside0LastX128/feeGrowthInside1LastX128

openPosition() reads stale feeGrowthInside values from Uniswap's positions() before collectLiquidity(), causing borrowers to overpay fees on closePosition().

- Payout: Contest reward
- Severity: high
- Program: Particle Protocol
- Credited: bin2chen
- Disclosed: 2024-01-26
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2023-12-particle-findings/issues/28
- Library page: https://bugbountylibrary.com/r/c4%3A2023-12-particle%3AH-02
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:business-logic

**Target:** ParticlePositionManager openPosition/closePosition leverage flow over Uniswap V3 positions
**Root cause:** openPosition() caches feeGrowthInside0LastX128/feeGrowthInside1LastX128 from UNI_POSITION_MANAGER.positions(tokenId) before decreaseLiquidity/collectLiquidity sync them, so the stored lien values are stale and lower than actual.
**Steps:**
1. Call openPosition() with a tokenId whose Uniswap position has accrued fees not yet collected.
2. Observe prepareLeverage() reads positions(tokenId) feeGrowthInside values before decreaseLiquidity/collectLiquidity.
3. Perform a swap accruing fees; positions() values remain unchanged until collect().
4. Call collectLiquidity() and re-read positions(): feeGrowthInside0LastX128 increases (e.g. +210658671188101898213195416).
5. closePosition() computes owed fees from the stale stored value, yielding a larger delta and extra fees charged to borrower.
**How found:** Code reading of openPosition()/prepareLeverage() showed feeGrowthInside values sourced from UNI_POSITION_MANAGER.positions() before any collect; a fork test confirmed positions() does not update after swap() until collectLiquidity() is called.
**Exploitation:** Borrower opens a leveraged position against a Uniswap V3 NFT with uncollected fee growth; the stale feeGrowthInside snapshot is stored in the lien. On closePosition(), getOwedFee() computes a larger fee delta than actually owed, and the borrower pays the excess to the LP.
**Insight:** Uniswap V3 positions() feeGrowthInside fields only refresh on increase/decrease/collect; any protocol snapshotting them for later fee accounting must force a collect first or it will misprice fees.
