Root Cause
EtherVista: 112-bit Reserve Overflow in K-Invariant
EtherVista's AMM swap function checks the constant-product invariant (k = reserve0 * reserve1) before and after each swap. Its reserves are stored as uint112, matching Uniswap V2's layout. When computing the product, the intermediate value can overflow the underlying calculation type:
// Simplified representation of the vulnerable invariant check
uint112 reserve0; // 112-bit
uint112 reserve1; // 112-bit
function swap(uint amount0Out, uint amount1Out, ...) external {
// If balance0Adjusted * balance1Adjusted overflows uint224,
// it wraps to a small number — easily >= k_before
require(
uint(balance0Adjusted) * uint(balance1Adjusted) >= uint(reserve0) * uint(reserve1),
"K invariant violated"
);
// ...
}
By manipulating the swap amounts so that the product overflows and wraps to a small value, the check can be satisfied even when the pool has been drained. The attacker also registered a controlled contract as an authorized router, then executed two transactions exploiting the overflowed invariant.
Loss: ~$18,600 in WETH and VISTA tokens.
BitBay: Zero-Liquidity Boundary Condition in Vault Withdrawal
BitBay's UsdcDaiV4Vault on Polygon implements a _withdraw() function that contains a boundary-condition flaw: when liquidity reaches zero, the function transfers the contract's entire token balance rather than the caller's proportional share:
function _withdraw(uint256 shares) internal returns (uint256 amount) {
if (liquidity == 0) {
// BUG: transfers full balance regardless of shares
amount = IERC20(token).balanceOf(address(this));
} else {
amount = (shares * liquidity) / totalShares;
}
IERC20(token).transfer(msg.sender, amount);
}
The attacker called reposition() to reduce liquidity to zero, then redeemed a single minimum-unit share, causing the vault to transfer its entire DAI balance (~14,838 DAI).
Loss: 14,838.47 DAI ($14K).
Attack Steps
EtherVista
| Step | Action |
|---|---|
| 1 | Register attacker-controlled contract as authorized router in EtherVista |
| 2 | Craft swap amounts so balance0Adjusted * balance1Adjusted overflows uint224 and wraps below K |
| 3 | K-invariant check passes on the overflowed value |
| 4 | Pool tokens drained across two transactions |
BitBay
| Step | Action |
|---|---|
| 1 | Call reposition() on the USDC/DAI vault to drain liquidity to exactly zero |
| 2 | Call withdraw(minShares) — triggers the liquidity == 0 branch |
| 3 | Vault transfers full DAI balance (~14,838 DAI) to attacker |
Impact
- EtherVista: ~$18,600 (WETH + VISTA) on Ethereum
- BitBay: ~$14,000 (14,838 DAI) on Polygon
- Combined: ~$32,600 total
- Detection: Both incidents flagged by SlowMist within hours on October 9, 2026
Lessons for Auditors
- Uniswap V2-style K-invariant checks require 256-bit intermediates. Reserve products of two
uint112values require auint256intermediate. Failing to explicitly cast before multiplication allows overflow. UseSafeUintor explicitly computeuint256(reserve0) * uint256(reserve1). - Test every boundary condition, especially zero values. The
liquidity == 0branch in BitBay was a fallback path that likely existed for initialization or emergency use — but it was triggered trivially by an attacker. All edge conditions should be fuzz-tested with extreme values including zero. - Authorization for routers in AMMs must use immutable or governance-guarded allowlists. EtherVista allowed an arbitrary address to be registered as a router, enabling the attacker to shape the swap context. Restrict router registration to multisig or timelock-controlled governance.
- Bounded vault payouts. Any vault withdrawal function should cap payout at
min(calculated_amount, contract_balance)and cross-check against the caller's share entitlement regardless of code path.