BitBay UsdcDaiV4Vault on Polygon drained for 14838.47 DAI when a single 1-wei share hit the `liquidity == 0` withdraw branch after a public `reposition()` rounded the new LP position to zero liquidity.
Security ·
Logged as “2026-10-09: The Usdcdaiv4Vault _Withdraw Liquidity0 Branch Pays The Vault Entire Dai And Usdc Balance To The Caller With No Share Weighted Cap And Reposition Is Permissionless Once The Tick Is Outside The Narrow Lp Range So The Attacker Deposits One Wei For A Single Share Swaps The Uniswap V4 Pool To Push The Tick Onto Tickupper Calls Reposition To Recompute A Position Whose Liquidity Rounds To Zero And Leaves The Pooled Funds As Raw Balance Then Redeems That One Share To Sweep The Whole 14838 Dai Pool on BitBay”.
The UsdcDaiV4Vault contract holds the pooled DAI and USDC of a Uniswap v4 LP position. Holders deposit to mint vault shares and redeem shares against the position's underlying. Two flaws line up.
First, the liquidity == 0 branch of _withdraw pays the entire vault balance to the caller, regardless of how many shares they hold. Inside that branch, the function reads daiBal = DAI.balanceOf(address(this)) and usdcBal = USDC.balanceOf(address(this)) and forwards both balances with DAI.transfer(recipient, daiBal) and USDC.transfer(recipient, usdcBal). shareAmount only reaches the event; the transfers are unconditional. A holder of a single 1-wei share can sweep the whole pool. The proportional path is only reachable when liquidity > 0.
Second, reposition() is a plain external function with no owner, keeper, role, or signature gate. The minter-only guard is inactive (mintThis is false). The only precondition is that the current pool tick is outside the vault's narrow [tickLower, tickUpper) LP range. After the call, the vault's liquidity state goes to zero while the former position value stays as raw token balance in the vault.
The attacker is a plain EOA driving a helper contract, deployed inside the attack transaction. First, deposit(1) mints a single share, contributing one wei of DAI to the pool so the deposit function will not revert. Next, the attacker swaps DAI to USDC on the v4 pool, pushing the tick above tickUpper and out of the vault's narrow [tickLower, tickUpper) range. With the tick out of range, a public call to reposition() recomputes a new position whose liquidity rounds to zero and leaves the pooled DAI and USDC sitting in the vault as raw token balance. The attacker then calls withdraw(1, deadline, false), which takes the liquidity == 0 branch and sweeps the whole balance to the attacker. No owner, admin, keeper, or signature is used anywhere in the sequence.
DeFiHackLabs records a loss of 14838.466608989135865711 DAI swept from the vault by a single 1-wei share on Polygon, matching the on-chain Withdrawn amount to the wei, with the attacker then unwinding the drained DAI plus the swap capital to a net ~14,813 USDC profit. The on-chain attack transaction is 0x5b795dc5c5e1665b712c7ccf478c1d8c59a162d698f4e0bfb40bd303e04a7ff2, mined on 2026-10-09.
Two changes would have stopped this. The liquidity == 0 branch of _withdraw should pay only the caller's proportional share of the raw balance, not the whole balance, and reposition() should require an owner, keeper, or time-locked role before it can be called permissionlessly on the public LP range.
This shares a structural shape with the KolanHook write-up: a permissionless public entry point that lets an attacker reach a state (liquidity == 0 here, expired windows there) where the next call pays out funds that are not theirs. The two protocols are unrelated, but the pattern is the same.
Query every incident yourself — free, no signup
Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.