Deploy scripts (DeployLocal.sol and DeployBase.sol) now execute a
seed buy + double-recenter sequence before handing control to users:
1. Temporarily grant deployer recenterAccess (via self as feeDestination)
2. Fund LM with a small amount and call recenter() -> places thin positions
3. SeedSwapper executes a small buy, generating a non-zero WETH fee
4. Second recenter() hits the cumulativeVolume==0 bootstrap path with
ethFee>0 -> _recordVolumeAndPrice fires -> cumulativeVolume>0
5. Revoke recenterAccess and restore the real feeDestination
After deployment, cumulativeVolume>0, so the bootstrap path is
unreachable by external users and cannot be front-run by an attacker
inflating the initial VWAP anchor with a whale buy.
Also adds:
- tools/deploy-optimizer.sh: verification step checks cumulativeVolume>0
after a fresh local deployment
- test_vwapBootstrappedBySeedTrade() in VWAPFloorProtection.t.sol:
confirms the deploy sequence (recenter + buy + recenter) leaves
cumulativeVolume>0 and getVWAP()>0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace pool.observe() TWAP price source with current pool tick (pool.slot0()) sampled once per recenter
- Remove _getTWAPOrFallback() and TWAPFallback event (added by PR #575)
- _scrapePositions now takes int24 currentTick instead of uint256 prevTimestamp; price computed via _priceAtTick before the burn loop
- Volume weighting (ethFee * 100) is unchanged — fees proxy swap volume over the recenter interval
- Direction fix from #566 (shouldRecordVWAP only on sell events) is preserved
- Remove test_twapReflectsAveragePriceNotJustLastSwap (tested reverted TWAP behaviour)
- ORACLE_CARDINALITY / increaseObservationCardinalityNext retained for _isPriceStable()
- All 188 tests pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix unsafe int32 intermediate cast: int56(int32(elapsed)) → int56(uint56(elapsed))
to prevent TWAP tick sign inversion for intervals above int32 max (~68 years)
- Remove redundant lastRecenterTimestamp state variable; capture prevTimestamp
from existing lastRecenterTime instead (saves ~20k gas per recenter)
- Use pool.increaseObservationCardinalityNext(ORACLE_CARDINALITY) in constructor
instead of recomputing the pool address; extract magic 100 to named constant
- Add TWAPFallback(uint32 elapsed) event emitted when pool.observe() reverts
so monitoring can distinguish degraded operation from normal bootstrap
- Remove conditional bypass paths in test_twapReflectsAveragePriceNotJustLastSwap;
assert vwapAfter > 0 and vwapAfter > initialPriceX96 unconditionally
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add lastRecenterTimestamp to track recenter interval for TWAP
- Increase pool observation cardinality to 100 in constructor
- In _scrapePositions, use pool.observe([elapsed, 0]) to get TWAP tick
over the full interval between recenters; falls back to anchor midpoint
when elapsed==0 or pool.observe() reverts (insufficient history)
- Add test_twapReflectsAveragePriceNotJustLastSwap: verifies TWAP-based
VWAP reflects the average price across the recenter interval, not just
the last-swap anchor snapshot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _scrapePositions natspec: 'ETH inflow' → 'ETH outflow / price fell or at bootstrap'
- Inline comment above VWAP block: remove inverted 'KRK sold out / ETH inflow' rationale,
replace with a neutral forward-reference to recenter() where the direction logic lives
- VWAPFloorProtection.t.sol: remove unused Kraiken and forge-std/Test.sol imports
(both are already provided by UniSwapHelper)
- test_floorConservativeAfterBuyOnlyAttack: add assertFalse(token0isWeth) guard so a
future change to the setUp parameter cannot silently invert the gap-direction assertion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Investigation findings:
- VWAP WAS being fed during buy-only cycles (shouldRecordVWAP = true on ETH inflow / price rising).
Over 80 buy-recenter cycles VWAP converged toward the inflated current price.
- When VWAP ≈ currentTick, mirrorTick = currentTick + vwapDistance ≈ currentTick, placing
the floor near the inflated price. Adversary sells back through the high floor, extracting
nearly all LM ETH.
- Optimizer parameters (anchorShare, CI) were not the primary cause.
Fix (LiquidityManager.sol):
Flip shouldRecordVWAP from buy direction to sell direction. VWAP is now recorded only when
price falls (ETH outflow / sell events) or at initial bootstrap (cumulativeVolume == 0).
Buy-only attack cycles leave VWAP frozen at the historical baseline, keeping mirrorTick and
the floor conservatively anchored far from the inflated current price.
Also updated onchain/AGENTS.md to document the corrected recording direction.
Regression test (VWAPFloorProtection.t.sol):
- test_vwapNotInflatedByBuyOnlyAttack: asserts getVWAP() stays at bootstrap after N buy cycles.
- test_floorConservativeAfterBuyOnlyAttack: asserts floor center is far below inflated tick.
- test_vwapBootstrapsOnFirstFeeEvent: confirms bootstrap path unchanged.
- test_recenterSucceedsOnSellDirectionWithoutReverts: confirms sell-direction recenters work.
All 187 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>