Root cause
Set Protocol V1's RebalancingSetTokenV3.actualizeFee() is a public function with no access control — no onlyOwner, no governance gate, no role check. It is gated only by the set being in Default rebalance state.
The vulnerability is in how it recalculates unitShares after calling handleFees(). When the fee is zero, handleFees() mints nothing, but actualizeFee() still overwrites unitShares using ceiling division (divCeil), whereas issuance uses floor division:
// Issuance path (floor division) — produces unitShares = 4927:
unitShares = currentSetAmount.mul(naturalUnit).div(totalSupply());
// actualizeFee() path (ceiling division) — produces unitShares = 4928:
// handleFees() → calculateNewUnitShares():
unitShares = currentSetAmount.mul(naturalUnit).divCeil(totalSupply());
// Even when profitFee=0 and streamingFee=0, unitShares is bumped by 1
This 1-unit discrepancy is small per call, but flash loan leverage scales it.
Attack steps
| Step | Action |
|---|---|
| 1 | Flash-borrow ETH from Uniswap v4 via zero-fee PoolManager.unlock → unlockCallback |
| 2 | Issue rebalancing set tokens while unitShares = 4927 (floor) |
| 3 | Call actualizeFee() — bumps unitShares to 4928 (ceiling), no fee minted |
| 4 | Redeem same quantity of set tokens at the higher unitShares = 4928 |
| 5 | Receive more underlying collateral than was deposited — pocketing the rounding delta |
| 6 | Repay Uniswap v4 flash loan, keep profit |
Attack tx: 0xaca5f83ff3a206209f3d213c33d04e6febe345f6ad52ef476bd1c0ee1505ba65
Block: 26131887 (forked at 26131886)
Date: October 6, 2026
Chain: Ethereum mainnet
Impact
- Protocol: Set Protocol V1 (
RebalancingSetTokenV3) - Chain: Ethereum mainnet
- Loss:
5.083 ETH ($21,000 at time of exploit) - PoC merged into DeFiHackLabs repository (PR #1294) October 7, 2026
- No official Set Protocol response or patch confirmed at time of writing
Lessons for auditors
- Public fee-accrual functions are dangerous. Any function that recalculates share counts or exchange rates without minting must be restricted to a trusted caller (governance, keeper, or timed trigger).
- Floor vs ceiling division asymmetry is an exploit surface. When issuance and redemption use different rounding directions, profit from issue→redeem cycles is possible. Verify that both paths round in the same direction (or that the invariant is preserved).
- Flash loan amplification turns micro-rounding into macro-loss. A 1-unit
unitSharesshift sounds trivial, but combined with flash liquidity, it is scalable. Always calculate worst-case flash-loop profit in rounding-related logic. - Test
actualizeFee()with zero-fee settings. A zero-fee state is not a no-op in protocols that unconditionally recalculate shares; it must be tested explicitly.