Root Cause
UNCX Smart Token Vesting V2 contained a share-accounting bug in the release function: it disbursed tokens to the beneficiary but failed to decrement the position's share counter. A subsequent cancel call then subtracted the full unvested share count from the pool—using the stale, un-decremented value—producing a negative accounting discrepancy that left the pool over-credited and withdrawable by the attacker.
// Simplified pseudocode of the vulnerable pattern
function release(uint positionId) external {
Position storage pos = positions[positionId];
uint releasable = _computeReleasable(pos);
pool.totalTokens -= releasable;
token.transfer(msg.sender, releasable);
// BUG: pos.shares NOT decremented here
}
function cancel(uint positionId) external {
Position storage pos = positions[positionId];
uint unvestShares = pos.shares; // stale—as if nothing was released
// Double-credit: pool gets back tokens as if no release ever occurred
pool.totalTokens += pos.depositedTokens - alreadyReleased;
_burnPosition(pos);
}
Per Immunefi's security chief, the release function failed to subtract released shares from the position's counter, so a later cancellation subtracted the unvested portion again from the stale count. The attacker also donated tokens to enlarge the stale debit and maximize the accounting gap.
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Create vesting position | Attacker deposits tokens into a vesting schedule via the V2 contract |
| 2 | Call release | Releases vested tokens; share counter not decremented |
| 3 | Donate additional tokens | Inflates the pool balance, enlarging the stale accounting gap |
| 4 | Call cancel | Subtracts full original share count as if no release occurred; pool is over-credited |
| 5 | Drain via second position | Attacker-controlled second vesting redeems the over-credited balance |
| 6 | Repeat / copycats | 11 drain transactions total; MEV bots and copycat wallets joined |
Impact
~$1.49 million drained across four chains (Ethereum, BNB Chain, Base, Robinhood Chain). Ten tokens were affected. Zipcoin, a project whose token vesting ran on UNCX V2, announced permanent shutdown on October 22, 2026, citing this incident plus a separate breach of its development infrastructure. UNCX suspended new vesting creation and raised creation fees to 1,000 ETH as an emergency circuit breaker. LP lockers were unaffected.
Lessons for Auditors
- Decrement every counter on withdrawal. Any function that transfers tokens out of a shared pool must update all share/balance counters before returning. The ERC-4626
previewWithdraw → _burn → transferordering exemplifies the correct pattern. - Invariant check:
totalShares × pricePerShare == poolTokens. A post-releaseassertion would immediately catch the accounting gap before it could be exploited. - Beware cancel-after-partial-release patterns. When a position can be partially released and later canceled, verify the cancellation computes the unvested amount against the post-release share count, not the original.
- Audit cross-chain deployments uniformly. The identical bytecode was deployed on four chains simultaneously; the bug propagated to all deployments at once.
- Token donation as an amplifier. Check that donating tokens to a pool cannot distort the accounting invariant—a common pattern in vault exploits (cf. ERC-4626 inflation attacks).