Use Case & Applications

Ensures the constant product (k = x * y) is maintained in AMM pools, preventing unauthorized manipulation of pool reserves and potential flash loan attacks. Critical for Uniswap V2-style AMMs, DEX aggregators, lending protocols using AMM price oracles, and yield farming protocols providing liquidity. The constant product formula is fundamental to many AMM designs - any deviation could indicate a serious security issue or potential exploit.

Explanation

Monitors the product of reserves (k) in AMM pools to ensure it remains constant after transactions using:
  • forkPreState() / forkPostState(): Compare reserve product before and after transaction
  • getStateChanges(): Monitor reserve changes throughout transaction callstack
  • Direct validation that k = reserve0 * reserve1 remains unchanged
The assertion verifies both initial/final states and detects potential manipulations during transaction execution. 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";
// Aerodrome style pool interface

interface IAmm {
    function getReserves() external view returns (uint256, uint256);
}

contract ConstantProductAssertion is Assertion {
    function triggers() external view override {
        // Register triggers for both reserve slots for each assertion
        // This ensures we catch any modifications to either reserve
        registerStorageChangeTrigger(this.assertionConstantProduct.selector, bytes32(uint256(0)));
        registerStorageChangeTrigger(this.assertionConstantProduct.selector, bytes32(uint256(1)));
    }

    // Assert that the constant product (k = x * y) invariant is maintained
    // This is done by comparing the pre-state and post-state values
    //
    // NOTE: Due to current limitations in getStateChanges, there are no guarantees
    // regarding timing when comparing values from two stateChanges arrays throughout the callstack.
    // An ideal solution in the future would allow for comparing values at any given
    // point in time during the callstack, thus making it possible to prevent
    // potential intra-transaction manipulations of the constant product invariant.
    //
    // For example, a malicious actor could temporarily manipulate the reserves
    // to violate the invariant during the transaction, then restore them before
    // the end of the transaction to avoid detection in a pre/post comparison.
    function assertionConstantProduct() external {
        // TOOD: Use new call frame forking to fix this assertion
        // Get the assertion adopter address
        IAmm adopter = IAmm(ph.getAssertionAdopter());

        // Get pre-state reserves and calculate initial k
        ph.forkPreTx();
        (uint256 reserve0Pre, uint256 reserve1Pre) = adopter.getReserves();
        uint256 kPre = reserve0Pre * reserve1Pre;

        // Get post-state reserves and calculate final k
        ph.forkPostTx();
        (uint256 reserve0Post, uint256 reserve1Post) = adopter.getReserves();
        uint256 kPost = reserve0Post * reserve1Post;

        // Verify the final state maintains the constant product
        require(kPre == kPost, "Constant product invariant violated");
    }
}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.