UNCX's VestingManager drained for ~88,000 to 89,000 USD across five ERC-20s on Ethereum when `release()` and `cancel()` double-debited a global share count and the next holder's release paid out at the inflated rate.
Security ·
Logged as “2026-10-14: Vestingmanager Pools Every Deposit Of A Given Token In One Contract And Release Pays A Slice Of The Full Contract Balance Over A Global Share Count Release Debits That Global Count But Leaves The Positions Deposited Shares In Place And Cancel Then Debits The Same Shares A Second Time So The Attacker Flash Borrows Each Token Creates Self Owned Vestings Whose Tranches Are Already Elapsed And So Instantly Releasable Releases And Cancels Soft Helper Vestings To Shrink The Share Denominator While Donating Token Straight Into The Pool To Raise The Balance Then Releases A Payout Vesting Against The Corrupted Denominator And Inflated Balance To Over Withdraw The Other Vesters Backing Tokens Across Five Erc 20S on UNCXSmartTokenVesting”.
The VestingManager contract pools every deposit of a given token into a single contract balance, tracking each position by an abstract "share" count in _totalTokenShares. Holders expect their position's payout to scale with their own deposit, but release() pays a slice of the entire pool. The two lines that matter are balance = IERC20(schedule.token).balanceOf(address(this)) and amount = (releasableShares * balance) / _totalTokenShares[schedule.token].
The denominator _totalTokenShares[token] is shared across every holder of that token. Shrinking it inflates the per-share payout for every position that still holds shares, including one an attacker just created.
release() and cancel() double-debit that denominator on the same shares. release() reduces _totalTokenShares[token] by the releasable shares but leaves the position's _depositedShares untouched. cancel() later computes its debit from _depositedShares, not from schedule.released, so the same shares are subtracted a second time. The denominator falls below the shares actually backed by the pool, and the next position to release gets paid as if the pool were larger than it is.
A position whose tranches are already in the past is fully vested the moment it exists. _getReleasableShares() calls the calculator with block.timestamp, so a vesting created with end-times one second before the current block can be released in the same transaction it was created.
The attack is fully permissionless. Any address can call createVesting, topUpVesting, release, and cancel. Per token the attacker creates one "payout" vesting for the real target amount, creates one or two small helper vestings, releases the helpers (debit number one, while leaving the helper positions' _depositedShares intact), optionally donates spare token straight into the pool to raise the contract balance, tops the helpers up and cancels them (debit number two, on the same shares), and finally releases the payout vesting against the now-corrupted denominator and the inflated balance.
Working capital is flash-borrowed per token and repaid in the same leg. EDGEN comes from a Uniswap v3 pool at 1% fee, ZC and LO0P from a Uniswap v4 PoolManager unlock that uses transient storage to clear the loan fee-free, and pepecoin and PAAL from Uniswap v2 pair flash swaps at the standard 0.3% fee. Each leg is settled inside the same flow that captures the stolen token, so the loans net to zero (less the v3 and v2 premiums) and only the over-withdrawn backing tokens remain.
DeFiHackLabs records a loss of approximately 88,000 to 89,000 USD across five ERC-20s on Ethereum, retained after the per-token flash loans are repaid. Per-token retained amounts are EDGEN 55,601,598.708417972560603048, ZC 9,055,847.637887682268154756, pepecoin 40,155.284285661160205717, LO0P 65,372.217059159993437512, and PAAL 113,657.766933968. The on-chain attack transaction is 0x352065378c4a41b77e5b6a1e5b5616cd2886c3e1a2c87ed3db8e5ddddc23823d, mined on 2026-10-14.
Two local changes would have stopped this. release() should pay against the position's own deposit, not a global share count, and cancel()'s share removal should be derived from schedule.released rather than from _depositedShares so the same shares cannot be debited twice.
Query every incident yourself — free, no signup
Updated 10 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.