// SPDX-License-Identifier: MIT pragma solidity 0.8.28; interface IUniFactoryL { function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address); } interface IUniPoolL { function slot0() external view returns (uint160, int24, uint16, uint16, uint16, uint8, bool); function tickSpacing() external view returns (int24); } /// @title UniTick — the tick-band arithmetic of UniswapAdapter, in a library. /// /// PUBLIC, so it is DELEGATECALLED rather than inlined: the point is to take /// this bytecode OUT of UniswapAdapter, which sits over EIP-170. It touches no /// storage and holds none, so delegatecall vs call is immaterial to /// correctness; the entry points are `public` only so solc leaves them in the /// deployed library instead of copying them into every caller. /// /// The behaviour is verbatim UniswapAdapter's: `test/UniswapAdapter.t.sol` /// exercises rebalance/reposition/split, which all route through here. library UniTick { uint32 internal constant TWAP_WINDOW = 300; /// Same bound SafeExit uses: ~2%. A keeper-authored `tickSlippage` must /// not vacate the close_at_tick gate. H-6. int24 internal constant MAX_UNWIND_TICK_DRIFT = 200; error NoTwap(address pool); error PoolMoved(int24 spot, int24 twap); /// The current tick and the pool's spacing, TWAP over TWAP_WINDOW. function currentTick(address factory, address token0, address token1, uint24 fee) public view returns (int24 tick, int24 spacing) { address pool = IUniFactoryL(factory).getPool(token0, token1, fee); spacing = IUniPoolL(pool).tickSpacing(); tick = twapTick(pool); } /// Cap a signed tickSlippage so it cannot swallow the TWAP gate. H-6. function capSlippage(int24 slip) public pure returns (int24) { if (slip < 0) slip = -slip; return slip > MAX_UNWIND_TICK_DRIFT ? MAX_UNWIND_TICK_DRIFT : slip; } /// Spot vs TWAP: a same-tx push large enough to matter moves slot0 off /// the average. Required before any liquidity decrease. H-6. function requireSettled(address factory, address token0, address token1, uint24 fee) public view { address pool = IUniFactoryL(factory).getPool(token0, token1, fee); (, int24 spot,,,,,) = IUniPoolL(pool).slot0(); int24 twap = twapTick(pool); int24 drift = spot > twap ? spot - twap : twap - spot; if (drift > MAX_UNWIND_TICK_DRIFT) revert PoolMoved(spot, twap); } /// The anchor for a one-spacing band, which needs both ticks: the tick used /// is whichever of TWAP and spot puts the band FURTHER from the price, which /// is also the manipulation-safe direction (C-4 of the 2026-09-10 audit). function bandAnchor( address factory, address token0, address token1, uint24 fee, bool token0Only ) public view returns (int24 tick, int24 spacing) { address pool = IUniFactoryL(factory).getPool(token0, token1, fee); spacing = IUniPoolL(pool).tickSpacing(); tick = twapTick(pool); (, int24 spot,,,,,) = IUniPoolL(pool).slot0(); if (token0Only) { if (spot > tick) tick = spot; } else if (spot < tick) { tick = spot; } } /// One spacing fully above current (`token0Only`) or fully below. function outOfRangeBand(int24 tick, int24 spacing, bool token0Only) public pure returns (int24 lower, int24 upper) { if (token0Only) { lower = roundUp(tick, spacing); if (lower <= tick) lower += spacing; upper = lower + spacing; } else { upper = roundDown(tick, spacing); lower = upper - spacing; } } // ── internal, inlined within this library ─────────────────────────────── function twapTick(address pool) internal view returns (int24) { // observe(uint32[]) — one offset, a length of two, then the two ages. // A RAW STATICCALL: `observe` returns two dynamic arrays and solc's // decoder for that costs about a kilobyte; the layout is fixed and read // by hand. Both cumulative words arrive sign-extended to 32 bytes. (bool ok, bytes memory ret) = pool.staticcall( abi.encodeWithSelector( 0x883bdbfd, uint256(0x20), uint256(2), uint256(TWAP_WINDOW), uint256(0) ) ); if (!ok || ret.length < 0xA0) revert NoTwap(pool); int256 older; int256 newer; assembly { older := mload(add(ret, 0x80)) newer := mload(add(ret, 0xA0)) } int256 delta = newer - older; int256 window = int256(uint256(TWAP_WINDOW)); int256 tick = delta / window; // Solidity truncates toward zero; the pool rounds a negative average DOWN. if (delta < 0 && delta % window != 0) tick = tick - 1; return int24(tick); } function roundDown(int24 tick, int24 spacing) internal pure returns (int24) { int24 compressed = tick / spacing; if (tick < 0 && tick % spacing != 0) compressed--; return compressed * spacing; } function roundUp(int24 tick, int24 spacing) internal pure returns (int24) { int24 down = roundDown(tick, spacing); return down == tick ? tick : down + spacing; } }