Use Case & Applications

Ensures prices remain within valid tick boundaries in concentrated liquidity AMMs, preventing price manipulation and maintaining protocol mathematical validity. Critical for AMMs using concentrated liquidity models (Uniswap V3, PancakeSwap V3), protocols with tick-based price calculations, DeFi protocols interacting with tick-based AMMs, and cross-chain bridges using tick-based price feeds. If attackers could push prices outside valid tick ranges, they could execute trades at invalid prices, manipulate liquidity positions, or cause protocol calculations to fail.

Explanation

Implements focused tick integrity verification:
  • ph.forkPreState() / ph.forkPostState(): Capture tick state before and after transaction
  • registerStorageChangeTrigger(): Monitor changes to tick-related storage slots
  • Verify ticks stay within global bounds (-887272 to 887272)
  • Ensure ticks align with pool’s tick spacing requirements
The assertion maintains proper concentrated liquidity calculations and prevents position management errors by enforcing tick boundaries and spacing rules. For more information about cheatcodes, see the Cheatcodes Documentation.

Code Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Assertion} from "credible-std/Assertion.sol";
import {PhEvm} from "credible-std/PhEvm.sol";

contract PriceWithinTicksAssertion is Assertion {
    // Uniswap V3 tick bounds
    int24 constant MIN_TICK = -887272;
    int24 constant MAX_TICK = 887272;

    function triggers() external view override {
        // Register trigger for changes to the tick storage slot
        // Assumes tick is stored in slot0 of the pool contract
        registerStorageChangeTrigger(this.priceWithinTicks.selector, bytes32(uint256(0)));
    }

    // Check that the price is within the tick bounds and that the tick is divisible by the tick spacing
    function priceWithinTicks() external {
        // Get the assertion adopter address
        IUniswapV3Pool adopter = IUniswapV3Pool(ph.getAssertionAdopter());

        // Get post-swap state
        ph.forkPostTx();
        (, int24 postTick,,,,,) = adopter.slot0();
        int24 spacing = adopter.tickSpacing();

        // Check 1: Tick must be within global bounds
        require(postTick >= MIN_TICK && postTick <= MAX_TICK, "Tick outside global bounds");

        // Check 2: Tick must be divisible by tickSpacing
        require(postTick % spacing == 0, "Tick not aligned with spacing");
    }
}

// Uniswap v3 pool style interface
interface IUniswapV3Pool {
    function slot0()
        external
        view
        returns (
            uint160 sqrtPriceX96,
            int24 tick,
            uint16 observationIndex,
            uint16 observationCardinality,
            uint16 observationCardinalityNext,
            uint8 feeProtocol,
            bool unlocked
        );
    function tickSpacing() external view returns (int24);
}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.