# Low Liquidity in Uniswap V3 Pool Can Lead to ETH Lockup in JBXBuybackDelegate Contract

JBXBuybackDelegate swaps ETH via Uniswap V3 with extreme sqrtPriceLimitX96; insufficient pool liquidity halts the swap early, permanently locking unused ETH in the contract.

- Payout: Audit contest
- Severity: medium
- Program: Juicebox
- Found by: minhquanym
- Disclosed: 2023-07-26
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2023-05-juicebox#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2023-05-juicebox%3AM-02
- Tags: area:web3 domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity vuln:accounting-error vuln:business-logic

**Target:** JBXBuybackDelegate.sol ETH-to-token swap via Uniswap V3 pool
**Root cause:** _swap() sets sqrtPriceLimitX96 to TickMath.MAX/MIN_SQRT_RATIO so the pool swap stops when price hits the limit; uniswapV3SwapCallback only wraps/transfers _amountToSend, leaving any msg.value surplus stranded in the contract with no refund path.
**Steps:**
1. Terminal calls JBXBuybackDelegate.didPay() with ETH to swap for project tokens.
2. _swap() calls pool.swap with `sqrtPriceLimitX96: _projectTokenIsZero ? TickMath.MAX_SQRT_RATIO - 1 : TickMath.MIN_SQRT_RATIO + 1` and amountSpecified = full ETH value.
3. If pool liquidity is thin or price was manipulated, the Uniswap loop exits when `state.sqrtPriceX96 == sqrtPriceLimitX96` before consuming all input.
4. uniswapV3SwapCallback wraps and transfers only `_amountToSend` WETH to the pool; the remaining ETH stays in the delegate contract.
5. No refund logic returns the surplus to the beneficiary, so it is permanently locked.
**How found:** Reviewed the swap path and noticed sqrtPriceLimitX96 is set to the extreme min/max rather than a slippage-bounded value, then traced the Uniswap V3 swap loop's price-limit exit condition and the callback's partial WETH transfer to conclude unused ETH is never refunded.
**Exploitation:** Precondition: low-liquidity or manipulated Uniswap V3 pool. A payment routed through didPay triggers a swap that halts at the price limit, consuming only part of the ETH; the surplus is trapped in the contract with no withdrawal function, causing permanent fund loss for the payer.
**Insight:** When a contract swaps with an extreme sqrtPriceLimitX96 to 'use all liquidity', check whether the callback refunds msg.value minus the actual amount consumed; partial fills at the price limit strand the remainder unless an explicit refund to the beneficiary exists.

