Root cause
Two independent protocol exploits were flagged by SlowMist on October 9, 2026. Both share a pattern of missing invariant enforcement at edge-case inputs.
BitBay (Polygon) — Zero-Liquidity Vault Drain
BitBay's vault _withdraw() function contained a special-case branch for when available liquidity was zero. Instead of reverting or returning zero, the branch transferred the contract's entire token balance to the caller:
// Vulnerable _withdraw() — reconstructed from on-chain analysis
function _withdraw(uint256 shares) internal {
uint256 availableLiquidity = getAvailableLiquidity();
if (availableLiquidity == 0) {
// BUG: sends full balance to any caller, regardless of shares held
token.safeTransfer(msg.sender, token.balanceOf(address(this)));
return;
}
uint256 amount = (shares * availableLiquidity) / totalShares;
token.safeTransfer(msg.sender, amount);
}
The attacker first called reposition() to zero out available liquidity, then redeemed one minimal share unit to trigger the vulnerable branch.
EtherVista DEX — uint112 K-Invariant Overflow in swap()
EtherVista's AMM stores pool reserves as uint112 (max ≈ 5.19 × 10³³). In the swap() function, multiplying two large uint112 reserves can overflow, causing the K-invariant product to wrap around to a smaller value — making a post-swap check (k_after >= k_before) pass falsely:
// Simplified vulnerable K-check (uint112 overflow)
uint112 reserve0;
uint112 reserve1;
// If reserves are large enough, the 224-bit intermediate overflows
uint256 balance0Adjusted = ...; // derived from uint112
uint256 balance1Adjusted = ...;
// With insufficient precision guards, this wraps and passes when it should fail:
require(balance0Adjusted * balance1Adjusted >= uint256(reserve0) * uint256(reserve1), "K");
The attacker also registered a self-controlled address as an authorised router, giving them the ability to call the swap function with crafted parameters.
Attack steps
| # | Protocol | Action |
|---|---|---|
| 1 | BitBay | Attacker calls reposition() to drain available liquidity to zero |
| 2 | BitBay | Attacker redeems 1 minimal share unit via _withdraw() |
| 3 | BitBay | Zero-liquidity branch triggers; vault transfers full DAI balance (14,838.47 DAI) to attacker |
| 4 | EtherVista | Attacker registers self-controlled contract as an authorised router |
| 5 | EtherVista | Reserves manipulated to hit uint112 multiplication overflow boundary |
| 6 | EtherVista | K-invariant check passes falsely; WETH and VISTA drained (~$18,600) |
Impact
- BitBay:
14,838.47 DAI ($14,838) drained; chain: Polygon - EtherVista: ~$18,600 in WETH and VISTA drained
- Combined loss: ~$33,438
- Flags: SlowMist flagged both incidents on October 9, 2026
- No post-mortems from either protocol had been published at time of reporting
Lessons for auditors
- Never let zero-total-supply / zero-liquidity paths transfer
balanceOf: Any fallback that pays out the entire contract balance when a denominator is zero creates a trivially exploitable donation-then-drain pattern. Revert or return zero shares instead. - Check all uint112 overflow paths in AMM reserve math: Solidity 0.8's overflow protection catches
uint256arithmetic but not narrower types in all casting paths. Multiplying twouint112values requires explicit 256-bit intermediate storage and careful invariant formulation. - Authorised-router registration deserves a time-lock: Allowing arbitrary contracts to register as routers removes the AMM's invariant enforcement. Use a whitelist with governance delay, not open registration.
- Fuzz at boundary values: Fuzz both reserve values near
type(uint112).maxto catch K-invariant failures before deployment. - reposition()/rebase() functions are high-risk: Any function that legitimately zeros available liquidity creates edge cases in all downstream logic. Audit every code path that reads
liquidity == 0.