@@ -39,6 +39,7 @@ import {PriceHelper} from "../../src/pool-bin/libraries/PriceHelper.sol";
3939import {BinHelper} from "../../src/pool-bin/libraries/BinHelper.sol " ;
4040import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol " ;
4141import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol " ;
42+ import {Uint128x128Math} from "../../src/pool-bin/libraries/math/Uint128x128Math.sol " ;
4243
4344contract BinPoolManagerTest is Test , BinTestHelper {
4445 using SafeCast for uint256 ;
@@ -305,6 +306,87 @@ contract BinPoolManagerTest is Test, BinTestHelper {
305306 poolManager.initialize (key, 0 );
306307 }
307308
309+ // -----------------------------------------------------------------------
310+ // activeId = 0 analysis: implicit defense in BinPoolManager.initialize
311+ // See: docs/activeId-price-relationship.md §9
312+ // -----------------------------------------------------------------------
313+
314+ /// @notice When activeId=0, pow() inside getPriceFromId reverts because the
315+ /// absolute value of the exponent (8,388,608) exceeds the 2^20 limit,
316+ /// throwing Uint128x128Math__PowUnderflow rather than an explicit
317+ /// input-validation error. The guard is a math side-effect, not an
318+ /// intentional parameter check.
319+ function test_InitializeActiveIdZero_RevertsWithPowUnderflow () public {
320+ // base = 1 + binStep/10000 expressed as a 128.128 fixed-point number
321+ // binStep = 10 (set in setUp via poolParam.setBinStep(10))
322+ uint256 base = Constants.SCALE + (uint256 (10 ) << Constants.SCALE_OFFSET) / Constants.BASIS_POINT_MAX;
323+ // exponent = 0 - REAL_ID_SHIFT = -8,388,608
324+ int256 exponent = - int256 (uint256 (1 << 23 ));
325+
326+ vm.expectRevert (abi.encodeWithSelector (Uint128x128Math.Uint128x128Math__PowUnderflow.selector , base, exponent));
327+ poolManager.initialize (key, 0 );
328+ }
329+
330+ /// @notice After initialize(key, 0) reverts, the pool slot0 remains all-zero
331+ /// (never written). getSlot0 returning activeId=0 means "never initialized",
332+ /// not "initialized with id 0". These two states are indistinguishable
333+ /// on-chain, which is the root design flaw.
334+ function test_InitializeActiveIdZero_PoolStateUnchanged () public {
335+ // Attempt to initialize with activeId=0; expect revert
336+ vm.expectRevert ();
337+ poolManager.initialize (key, 0 );
338+
339+ // Pool was never written: slot0 is still all-zero
340+ (uint24 storedActiveId ,,) = poolManager.getSlot0 (key.toId ());
341+ assertEq (storedActiveId, 0 , "pool should remain uninitialized (slot0 = 0) " );
342+
343+ // A valid activeId on the same key initializes successfully
344+ uint24 validId = 2 ** 23 ; // price == 1
345+ poolManager.initialize (key, validId);
346+ (storedActiveId,,) = poolManager.getSlot0 (key.toId ());
347+ assertEq (storedActiveId, validId, "pool should be initialized with valid activeId " );
348+ }
349+
350+ /// @notice The usable activeId range is bounded by fixed-point precision of
351+ /// base^exponent, not merely by the |exponent| < 2^20 gate in pow().
352+ /// For binStep=10 (base≈1.001), pow() underflows to zero at |exponent|≈88,767.
353+ /// activeId=0 has exponent=-8,388,608, roughly 94x beyond that practical limit.
354+ function test_InitializeActiveIdZero_BoundaryComparison () public {
355+ uint24 REAL_ID_SHIFT = uint24 (1 << 23 ); // 8,388,608 — the price-equals-1 anchor
356+
357+ // --- Practical minimum valid activeId for binStep=10 (offset -88,767) ---
358+ // Confirmed by test_fuzz_ValidPrice; asserted here for explicitness
359+ uint24 practicalMinValid = REAL_ID_SHIFT - 88_767 ; // 8,299,841
360+ poolManager.initialize (key, practicalMinValid);
361+ (uint24 storedId ,,) = poolManager.getSlot0 (key.toId ());
362+ assertEq (storedId, practicalMinValid, "practicalMinValid should initialize successfully " );
363+
364+ // --- One step beyond the practical boundary: pow precision underflow, revert ---
365+ PoolKey memory key2 = PoolKey ({
366+ currency0: currency0,
367+ currency1: currency1,
368+ hooks: IHooks (address (0 )),
369+ poolManager: IPoolManager (address (poolManager)),
370+ fee: uint24 (500 ),
371+ parameters: poolParam.setBinStep (10 )
372+ });
373+ uint24 practicalMinInvalid = REAL_ID_SHIFT - 88_768 ; // 8,299,840
374+ vm.expectRevert ();
375+ poolManager.initialize (key2, practicalMinInvalid);
376+
377+ // --- activeId=0: exponent=-8,388,608, ~94x beyond the practical limit, revert ---
378+ PoolKey memory key3 = PoolKey ({
379+ currency0: currency0,
380+ currency1: currency1,
381+ hooks: IHooks (address (0 )),
382+ poolManager: IPoolManager (address (poolManager)),
383+ fee: uint24 (100 ),
384+ parameters: poolParam.setBinStep (10 )
385+ });
386+ vm.expectRevert ();
387+ poolManager.initialize (key3, 0 );
388+ }
389+
308390 function testInitializeSwapFeeTooLarge () public {
309391 uint24 swapFee = LPFeeLibrary.TEN_PERCENT_FEE + 1 ;
310392
0 commit comments