Use Case & Applications

Prevents unauthorized fee manipulation in AMMs and DeFi protocols by using a whitelist approach to control fee changes. Critical for AMMs (Velodrome, Aerodrome, Curve), DEX aggregators, lending protocols with origination fees, yield aggregators with performance fees, and cross-chain bridges with transfer fees. Unexpected fee changes can lead to unauthorized manipulation, protocol revenue loss, user losses, or market manipulation through fee bypasses.

Explanation

Uses a whitelist approach to control fee changes:
  • registerStorageChangeTrigger(): Detect changes to fee storage slot
  • ph.load(): Retrieve new fee value after change
  • getStateChangesUint(): Verify all state changes for fee storage slot
  • Maintains hardcoded allowed fee values for stable pools (0.1%, 0.15%) and non-stable pools (0.25%, 0.30%)
When triggered, verifies the new fee matches one of the allowed values, preventing unauthorized modifications while allowing proper protocol updates. 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";

// Check that fee invariants are maintained
contract AmmFeeVerificationAssertion is Assertion {
    // Hardcoded whitelist of allowed fee values
    uint256 private constant STABLE_POOL_FEE_1 = 1; // 0.1%
    uint256 private constant STABLE_POOL_FEE_2 = 15; // 0.15%
    uint256 private constant NON_STABLE_POOL_FEE_1 = 25; // 0.25%
    uint256 private constant NON_STABLE_POOL_FEE_2 = 30; // 0.30%

    function triggers() external view override {
        // Register trigger for changes to the fee storage slot
        registerStorageChangeTrigger(this.assertFeeVerification.selector, bytes32(uint256(1))); // Assuming fee is in slot 1
    }

    // Verify that any fee change is to an allowed value
    function assertFeeVerification() external view {
        // Get the assertion adopter address
        IPool adopter = IPool(ph.getAssertionAdopter());

        // Get the new fee value and pool type
        bool isStable = adopter.stable();
        uint256 newFee = uint256(ph.load(address(adopter), bytes32(uint256(1))));

        // Check if the new fee is in the whitelist
        bool isAllowed = isStable
            ? (newFee == STABLE_POOL_FEE_1 || newFee == STABLE_POOL_FEE_2)
            : (newFee == NON_STABLE_POOL_FEE_1 || newFee == NON_STABLE_POOL_FEE_2);

        require(isAllowed, "Fee change to unauthorized value");

        // If the simple check passes, verify no unauthorized changes in the callstack
        uint256[] memory changes = getStateChangesUint(address(adopter), bytes32(uint256(1)));

        // Check each change against the whitelist
        for (uint256 i = 0; i < changes.length; i++) {
            isAllowed = isStable
                ? (changes[i] == STABLE_POOL_FEE_1 || changes[i] == STABLE_POOL_FEE_2)
                : (changes[i] == NON_STABLE_POOL_FEE_1 || changes[i] == NON_STABLE_POOL_FEE_2);
            require(isAllowed, "Unauthorized fee change detected in callstack");
        }
    }
}

// Aerodrome style interface
interface IPool {
    function fee() external view returns (uint256);
    function stable() external view returns (bool);
}
Note: Full examples with tests available in the Phylax Assertion Examples Repository.