# TWAMM Instant Orders Can Steal Historical Rewards

TWAMM order created at block.timestamp==startTime skips startTime bitmap registration, so getRewardRateInside returns all historical rewards, letting attacker drain shared reward pool.

- Payout: Contest reward
- Severity: medium
- Program: Ekubo
- Credited: zzebra83
- Disclosed: 2026-01-12
- Write-up quality: 7/10
- Original: https://code4rena.com/reports/2025-11-ekubo#m-02
- Library page: https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:medium tech:evm tech:solidity technique:regression technique:source-review vuln:accounting-error vuln:business-logic

**Target:** TWAMM.sol handleForwardData / getRewardRateInside reward accounting
**Root cause:** In handleForwardData, when block.timestamp >= startTime the else branch only calls _updateTime for endTime, never registering startTime in the time bitmap, so rewardRatesBeforeSlot[startTime] stays 0 and getRewardRateInside computes currentRate - 0.
**Steps:**
1. Warp to a timestamp where block.timestamp % 256 == 0 and pool has accumulated historical rewards.
2. Create order with startTime == block.timestamp and endTime = startTime + 256 via `orders.mintAndIncreaseSellAmount(key, 1e18, max)`.
3. Advance to endTime and call `twamm.lockAndExecuteVirtualOrders(poolKey)`.
4. Call `twamm.getRewardRateInside(poolId, key.config)` — returns inflated value (~199% of a comparable legitimate order).
5. Call `orders.collectProceeds(orderId, key, attacker)` before victim; pool drained, victim's collectProceeds reverts.
**How found:** Code review of handleForwardData's branch on block.timestamp < startTime vs else; noticed the == case falls into else and skips _updateTime(startTime), leaving rewardRatesBeforeSlot uninitialized; confirmed via forge PoC comparing rewardRateInside of exploit vs legitimate order.
**Exploitation:** Attacker waits for a 256-second boundary, submits a tiny order (1e18 vs victim's 100e18) with startTime==block.timestamp, then collects proceeds first. Inflated rewardRateInside credits all historical rewards, draining the shared pool; victim's later collectProceeds reverts (SavedBalanceOverflow).
**Insight:** When auditing time-indexed accounting (bitmaps, checkpoints, TWAP slots), test the boundary where the event time equals the current block time — off-by-one branch conditions that skip initialization of a checkpoint leave a zero baseline that turns a delta calculation into an absolute value.
